Everything-claude-code laravel-tdd
使用 PHPUnit 和 Pest、工厂、数据库测试、模拟以及覆盖率目标进行 Laravel 的测试驱动开发。
install
source · Clone the upstream repo
git clone https://github.com/affaan-m/everything-claude-code
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/affaan-m/everything-claude-code "$T" && mkdir -p ~/.claude/skills && cp -r "$T/docs/zh-CN/skills/laravel-tdd" ~/.claude/skills/affaan-m-everything-claude-code-laravel-tdd-9917e5 && rm -rf "$T"
manifest:
docs/zh-CN/skills/laravel-tdd/SKILL.mdsource content
Laravel TDD 工作流
使用 PHPUnit 和 Pest 为 Laravel 应用程序进行测试驱动开发,覆盖率(单元 + 功能)达到 80% 以上。
使用时机
- Laravel 中的新功能或端点
- 错误修复或重构
- 测试 Eloquent 模型、策略、作业和通知
- 除非项目已标准化使用 PHPUnit,否则新测试首选 Pest
工作原理
红-绿-重构循环
- 编写一个失败的测试
- 实施最小更改以通过测试
- 在保持测试通过的同时进行重构
测试层级
- 单元:纯 PHP 类、值对象、服务
- 功能:HTTP 端点、身份验证、验证、策略
- 集成:数据库 + 队列 + 外部边界
根据范围选择层级:
- 对纯业务逻辑和服务使用单元测试。
- 对 HTTP、身份验证、验证和响应结构使用功能测试。
- 当需要验证数据库/队列/外部服务组合时使用集成测试。
数据库策略
- 对于大多数功能/集成测试使用
(每次测试运行运行一次迁移,然后在支持时将每个测试包装在事务中;内存数据库可能每次测试重新迁移)RefreshDatabase - 当模式已迁移且仅需要每次测试回滚时使用
DatabaseTransactions - 当每次测试都需要完整迁移/刷新且可以承担其开销时使用
DatabaseMigrations
将
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']); });
工厂和状态
- 使用工厂生成测试数据
- 为边缘情况定义状态(已归档、管理员、试用)
$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' => 'alpha']); $found = Project::query()->where('slug', 'alpha')->firstOrFail(); $this->assertSame($project->id, $found->id); } }
副作用模拟
- 作业使用
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 和外部服务
- 使用
隔离外部 APIHttp::fake() - 使用
断言出站负载Http::assertSent()
覆盖率目标
- 对单元 + 功能测试强制执行 80% 以上的覆盖率
- 在 CI 中使用
或pcovXDEBUG_MODE=coverage
测试命令
php artisan testvendor/bin/phpunitvendor/bin/pest
测试配置
- 使用
设置phpunit.xml
和DB_CONNECTION=sqlite
以进行快速测试DB_DATABASE=:memory: - 为测试保持独立的环境,以避免触及开发/生产数据
授权测试
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 测试辅助函数来断言组件名称和属性。
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 响应一致。