還記得 30 天前,我們從一個簡單的 expect(1 + 1)->toBe(2)
開始嗎?
今天,我們不僅完成了完整的 Todo API,更重要的是建立了 TDD 思維。這趟旅程的終點,正是你成為更好開發者的起點。
今天我們要完成最後一塊拼圖:
開始 → 部署前檢查 → CI/CD 設置 → 生產環境測試
↓ ↓ ↓ ↓
Day 1 Day 10 Day 20 Day 30
[基礎] [進階] [實戰] [部署] ← 你在這裡!
進度:[████████████████████] 100% 🎉
在部署之前,讓我們確保所有測試都能順利通過:
# 執行所有測試
php artisan test
# 執行特定測試套件
php artisan test --testsuite=Feature
php artisan test --testsuite=Unit
# 生成測試覆蓋率報告
php artisan test --coverage --min=80
<?php
namespace Tests\Feature\Day30;
use Tests\TestCase;
use Illuminate\Support\Facades\Artisan;
it('passes all unit tests', function () {
$result = Artisan::call('test', [
'--testsuite' => 'Unit',
'--stop-on-failure' => true
]);
expect($result)->toBe(0);
});
it('passes all feature tests', function () {
$result = Artisan::call('test', [
'--testsuite' => 'Feature',
'--stop-on-failure' => true
]);
expect($result)->toBe(0);
});
it('has minimum test coverage', function () {
$output = shell_exec('php artisan test --coverage --min=80 2>&1');
expect($output)->not->toContain('Code coverage below');
});
it('has no pending migrations', function () {
$result = Artisan::call('migrate:status');
$output = Artisan::output();
expect($output)->not->toContain('Pending');
});
it('can compile assets without errors', function () {
$result = shell_exec('npm run build 2>&1');
expect($result)->toContain('built in');
expect($result)->not->toContain('ERROR');
});
name: Laravel CI/CD
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: laravel_test
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
coverage: xdebug
- name: Install Dependencies
run: |
composer install -q --no-ansi --no-interaction
php artisan key:generate
- name: Run Tests
run: php artisan test --coverage --min=80
- name: Deploy to Production
if: github.ref == 'refs/heads/main'
run: |
# Deployment commands here
php artisan config:cache
php artisan route:cache
# 準備部署腳本
#!/bin/bash
cd /home/forge/todo-api
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart
<?php
namespace Tests\Feature\Day30;
use Tests\TestCase;
it('validates critical endpoints', function () {
$endpoints = [
'/api/todos' => 200,
'/api/health' => 200,
'/api/invalid' => 404
];
foreach ($endpoints as $endpoint => $expectedStatus) {
$response = $this->get($endpoint);
expect($response->status())->toBe($expectedStatus);
}
});
it('monitors response time', function () {
$start = microtime(true);
$response = $this->get('/');
$duration = (microtime(true) - $start) * 1000;
expect($response->status())->toBe(200);
expect($duration)->toBeLessThan(500);
});
<?php
namespace Tests\Feature\Day30;
use Tests\TestCase;
it('has correct environment configuration', function () {
$requiredEnvVars = [
'APP_NAME', 'APP_ENV', 'APP_KEY', 'APP_URL',
'DB_CONNECTION', 'DB_HOST', 'DB_PORT', 'DB_DATABASE',
];
foreach ($requiredEnvVars as $var) {
expect(env($var))->not->toBeNull();
}
});
it('uses secure settings in production', function () {
if (app()->environment('production')) {
expect(config('app.debug'))->toBeFalse();
expect(config('app.url'))->toStartWith('https://');
expect(config('session.secure'))->toBeTrue();
}
});
// tests/Feature/Day30/PerformanceTest.php
<?php
namespace Tests\Feature\Day30;
use Tests\TestCase;
use Illuminate\Support\Facades\DB;
it('keeps database queries under threshold', function () {
DB::enableQueryLog();
$this->get('/api/todos');
$queryCount = count(DB::getQueryLog());
expect($queryCount)->toBeLessThanOrEqual(10);
});
it('maintains acceptable memory usage', function () {
$startMemory = memory_get_usage();
$this->post('/api/todos', ['title' => 'Test Todo']);
$memoryUsed = (memory_get_usage() - $startMemory) / 1024 / 1024;
expect($memoryUsed)->toBeLessThan(10);
});
<?php
namespace Tests\Feature\Day30;
use Tests\TestCase;
use Illuminate\Support\Facades\Log;
it('logs errors correctly', function () {
Log::shouldReceive('error')
->once()
->with('Application error', \Mockery::any());
// Trigger error logging
Log::error('Application error', ['test' => true]);
});
it('returns appropriate error responses', function () {
$response = $this->getJson('/api/invalid-endpoint');
expect($response->status())->toBe(404);
});
讓我們回顧這 30 天的學習旅程:
# 執行測試並產生覆蓋率報告
php artisan test --coverage --min=80
php artisan test --coverage-html=coverage
恭喜你完成了這 30 天的 TDD 學習之旅!你現在已經具備了:
今天我們完成了整個 TDD 學習旅程的最後一哩路!從部署前的檢查、CI/CD 設置,到生產環境的監控,我們建立了一個完整的測試驅動開發流程。
30 天前,我們從一個簡單的測試開始;30 天後,我們擁有了一個經過完整測試、可以自信部署的應用程式。TDD 不僅改變了我們寫程式的方式,更重要的是改變了我們思考問題的角度。
感謝你陪伴我走完這 30 天的旅程。願 TDD 成為你開發路上的明燈!
Happy Testing, Happy Coding! 🚀
這是 iThome 鐵人賽「Laravel Pest TDD 實戰」系列的最後一篇文章。希望這 30 天的內容對你有所幫助。歡迎在實際專案中應用所學,並持續精進你的測試技能!