🚀 快速安装

复制以下命令并运行,立即安装此 Skill:

npx skills add https://skills.sh/affaan-m/everything-claude-code/laravel-tdd

💡 提示:需要 Node.js 和 NPM

Laravel TDD 工作流

使用 PHPUnit 和 Pest 为 Laravel 应用程序进行测试驱动开发,达到 80% 以上的覆盖率(单元测试 + 功能测试)。

使用时机

  • 在 Laravel 中开发新功能或端点
  • 修复错误或重构
  • 测试 Eloquent 模型、策略、任务和通知
  • 除非项目已标准化使用 PHPUnit,否则新测试优先使用 Pest

工作原理

红-绿-重构循环

  1. 编写一个失败的测试
  2. 实现最小的更改以通过测试
  3. 在保持测试通过的情况下重构

测试层级

  • 单元测试:纯 PHP 类、值对象、服务
  • 功能测试:HTTP 端点、认证、验证、策略
  • 集成测试:数据库 + 队列 + 外部边界

根据范围选择层级:

  • 使用 单元测试 测试纯业务逻辑和服务。
  • 使用 功能测试 测试 HTTP、认证、验证和响应结构。
  • 使用 集成测试 在测试中同时验证数据库/队列/外部服务。

数据库策略

  • 对于大多数功能/集成测试,使用 RefreshDatabase(在支持的情况下,每个测试运行一次迁移,然后每个测试包装在一个事务中;内存数据库可能每次测试都重新迁移)
  • 当 schema 已经迁移且只需要每个测试回滚时,使用 DatabaseTransactions
  • 当需要为每个测试进行完整迁移/刷新且可以承受成本时,使用 DatabaseMigrations

对于需要接触数据库的测试,默认使用 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' => '新项目',
        ]);

        $response->assertCreated();
        $this->assertDatabaseHas('projects', ['name' => '新项目']);
    }
}

功能测试示例(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('所有者可以创建项目', function () {
    $user = User::factory()->create();

    $response = actingAs($user)->postJson('/api/projects', [
        'name' => '新项目',
    ]);

    $response->assertCreated();
    assertDatabaseHas('projects', ['name' => '新项目']);
});

功能测试 Pest 示例(HTTP 层)

use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

use function Pest\Laravel\actingAs;

uses(RefreshDatabase::class);

test('项目列表返回分页结果', 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 和外部服务

  • 使用 Http::fake() 隔离外部 API
  • 使用 Http::assertSent() 断言出站负载

覆盖率目标

  • 确保单元测试和功能测试的覆盖率达到 80% 以上
  • 在 CI 中使用 pcovXDEBUG_MODE=coverage

测试命令

  • php artisan test
  • vendor/bin/phpunit
  • vendor/bin/pest

测试配置

  • 使用 phpunit.xml 设置 DB_CONNECTION=sqliteDB_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 响应保持一致。

📄 原始文档

完整文档(英文):

https://skills.sh/affaan-m/everything-claude-code/laravel-tdd

💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。