
关于
Laravel 测试驱动开发,使用 PHPUnit 和 Pest,涵盖工厂、数据库测试、Fakes 和覆盖率目标。
name: laravel-tdd description: 使用 PHPUnit 和 Pest 进行 Laravel 测试驱动开发,包括工厂、数据库测试、Fakes 和覆盖率目标。 origin: ECC
Laravel TDD 工作流
使用 PHPUnit 和 Pest 进行 Laravel 应用的测试驱动开发,目标覆盖率 80%+(单元 + 功能测试)。
何时使用
- Laravel 中的新功能或端点
- Bug 修复或重构
- 测试 Eloquent 模型、策略、任务和通知
- 除非项目已标准化使用 PHPUnit,否则优先使用 Pest 编写新测试
工作原理
红-绿-重构循环
- 编写一个失败的测试
- 实现最小改动使其通过
- 在保持测试通过的同时重构
测试层级
- 单元测试:纯 PHP 类、值对象、服务
- 功能测试:HTTP 端点、认证、验证、策略
- 集成测试:数据库 + 队列 + 外部边界
根据范围选择层级:
- 使用单元测试测试纯业务逻辑和服务。
- 使用功能测试测试 HTTP、认证、验证和响应结构。
- 使用集成测试验证数据库/队列/外部服务的协同工作。
数据库策略
RefreshDatabase用于大多数功能/集成测试(每次测试运行执行一次迁移,然后在支持事务的情况下将每个测试包装在事务中;内存数据库可能每次测试都重新迁移)DatabaseTransactions当 schema 已迁移且只需要每次测试回滚时使用DatabaseMigrations当需要每次测试都完整 migrate/fresh 且能承受开销时使用
将 RefreshDatabase 作为涉及数据库的测试的默认选择:对于支持事务的数据库,它每次测试运行执行一次迁移(通过静态标志),并将每个测试包装在事务中;对于 :memory: SQLite 或不支持事务的连接,它在每次测试前迁移。当 schema 已迁移且只需要每次测试回滚时使用 DatabaseTransactions。
测试框架选择
- 可用时默认使用 Pest 编写新测试。
- 仅当项目已标准化使用 PHPUnit 或需要 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']);
});
工厂和状态
- 使用工厂生成测试数据
- 为边界情况定义状态(已归档、管理员、试用)
$user = User::factory()->state(['role' => 'admin'])->create();
数据库测试
- 使用
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' => 'test-project']);
$found = Project::where('slug', 'test-project')->first();
$this->assertNotNull($found);
$this->assertEquals($project->id, $found->id);
}
}
兼容工具
Claude CodeCursor
标签
数据工程

