Laravel TDD 工作流¶
使用 PHPUnit 和 Pest 为 Laravel 应用程序进行测试驱动开发。80%+ 覆盖率(单元测试 + 功能测试)。
使用时机¶
- 新的 Laravel 功能或端点
- 修复 bug 或重构
- 测试 Eloquent 模型、策略、任务、通知
- 除非项目已标准化使用 PHPUnit,否则新测试优先使用 Pest
工作原理¶
RED-GREEN-REFACTOR 循环¶
1) 编写失败的测试 2) 实现最小改动使其通过 3) 在保持测试绿色的同时重构
测试层¶
- 单元测试:纯 PHP 类、值对象、服务
- 功能测试:HTTP 端点、认证、验证、策略
- 集成测试:数据库 + 队列 + 外部边界
根据范围选择层:
- 单元测试用于纯业务逻辑和服务。
- 功能测试用于 HTTP、认证、验证、响应结构。
- 集成测试用于一起验证 DB/队列/外部服务时。
数据库策略¶
RefreshDatabase用于大多数功能/集成测试(每次测试运行执行一次迁移,然后将每个测试包裹在事务中;内存数据库可能每个测试重新迁移)DatabaseTransactions当模式已经迁移,且只需要每个测试回滚时DatabaseMigrations当每个测试都需要完整 migrate/fresh,或者可以承受成本时
将 RefreshDatabase 用作涉及数据库的测试的默认选项:对于支持事务的数据库,使用迁移步骤标志每次测试运行执行一次,然后将每个测试包裹在事务中;对于 :memory: SQLite 或非事务连接,则在每个测试之前迁移。如果模式已经迁移,且只需要每个测试回滚,则使用 DatabaseTransactions。
测试框架选择¶
- 新的测试默认使用 Pest。
- PHPUnit 仅当项目已经标准化使用它,或需要 PHPUnit 特定工具时使用。
示例¶
PHPUnit 示例¶
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class ProjectControllerTest extends TestCase
{
use RefreshDatabase;
public function test_owner_can_create_project(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->postJson('/api/projects', [
'name' => 'New Project',
]);
$response->assertCreated();
$this->assertDatabaseHas('projects', ['name' => 'New Project']);
}
}
功能测试示例(HTTP 层)¶
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class ProjectIndexTest extends TestCase
{
use RefreshDatabase;
public function test_projects_index_returns_paginated_results(): void
{
$user = User::factory()->create();
Project::factory()->count(3)->for($user)->create();
$response = $this->actingAs($user)->getJson('/api/projects');
$response->assertOk();
$response->assertJsonStructure(['success', 'data', 'error', 'meta']);
}
}
Pest 示例¶
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\actingAs;
use function Pest\Laravel\assertDatabaseHas;
uses(RefreshDatabase::class);
test('owner can create project', function () {
$user = User::factory()->create();
$response = actingAs($user)->postJson('/api/projects', [
'name' => 'New Project',
]);
$response->assertCreated();
assertDatabaseHas('projects', ['name' => 'New Project']);
});
功能测试 Pest 示例(HTTP 层)¶
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\actingAs;
uses(RefreshDatabase::class);
test('projects index returns paginated results', function () {
$user = User::factory()->create();
Project::factory()->count(3)->for($user)->create();
$response = actingAs($user)->getJson('/api/projects');
$response->assertOk();
$response->assertJsonStructure(['success', 'data', 'error', 'meta']);
});
工厂与状态¶
- 测试数据使用工厂
- 定义边缘情况(已归档、管理员、试用)的状态
数据库测试¶
- 干净状态使用
RefreshDatabase - 隔离测试并保持确定性
- 优先使用
assertDatabaseHas而不是手动查询
持久性测试示例¶
use App\Models\Project;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class ProjectRepositoryTest extends TestCase
{
use RefreshDatabase;
public function test_project_can_be_retrieved_by_slug(): void
{
$project = Project::factory()->create(['slug' => 'alpha']);
$found = Project::query()->where('slug', 'alpha')->firstOrFail();
$this->assertSame($project->id, $found->id);
}
}
用于副作用的 Fake¶
Bus::fake()任务用Queue::fake()队列作业用Mail::fake()和Notification::fake()通知用Event::fake()领域事件用
use Illuminate\Support\Facades\Queue;
Queue::fake();
dispatch(new SendOrderConfirmation($order->id));
Queue::assertPushed(SendOrderConfirmation::class);
use Illuminate\Support\Facades\Notification;
Notification::fake();
$user->notify(new InvoiceReady($invoice));
Notification::assertSentTo($user, InvoiceReady::class);
认证测试(Sanctum)¶
use Laravel\Sanctum\Sanctum;
Sanctum::actingAs($user);
$response = $this->getJson('/api/projects');
$response->assertOk();
HTTP 与外部服务¶
- 使用
Http::fake()隔离外部 API - 使用
Http::assertSent()断言发送的 payload
覆盖率目标¶
- 单元测试 + 功能测试实现 80%+ 覆盖率
- CI 中使用
pcov或XDEBUG_MODE=coverage
测试命令¶
php artisan testvendor/bin/phpunitvendor/bin/pest
测试配置¶
- 使用
phpunit.xml设置DB_CONNECTION=sqlite和DB_DATABASE=:memory:以快速测试 - 测试保持单独的 env,避免接触 dev/prod 数据
授权测试¶
use Illuminate\Support\Facades\Gate;
$this->assertTrue(Gate::forUser($user)->allows('update', $project));
$this->assertFalse(Gate::forUser($otherUser)->allows('update', $project));
Inertia 功能测试¶
使用 Inertia.js 时,使用 Inertia 测试助手断言组件名和 props。
use App\Models\User;
use Inertia\Testing\AssertableInertia;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class DashboardInertiaTest extends TestCase
{
use RefreshDatabase;
public function test_dashboard_inertia_props(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/dashboard');
$response->assertOk();
$response->assertInertia(fn (AssertableInertia $page) => $page
->component('Dashboard')
->where('user.id', $user->id)
->has('projects')
);
}
}
优先使用 assertInertia 而非原始 JSON 断言,使测试与 Inertia 响应保持一致。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 ev/ECC