Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Desarrollo guiado por pruebas para Laravel con PHPUnit y Pest, factories, pruebas de base de datos, fakes y objetivos de cobertura.
.claude/skills/affaan-m-laravel-tdd/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 50% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 68% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 66% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 53% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 65% | 0% |
使用 PHPUnit 和 Pest 为 Laravel 应用程序进行测试驱动开发,覆盖率(单元 + 功能)达到 80% 以上。
根据范围选择层级:
RefreshDatabase(每次测试运行运行一次迁移,然后在支持时将每个测试包装在事务中;内存数据库可能每次测试重新迁移)DatabaseTransactionsDatabaseMigrations将 RefreshDatabase 作为触及数据库的测试的默认选择:对于支持事务的数据库,它每次测试运行运行一次迁移(通过静态标志)并将每个测试包装在事务中;对于 :memory: SQLite 或不支持事务的连接,它在每次测试前进行迁移。当模式已迁移且仅需要每次测试回滚时使用 DatabaseTransactions。
phpuse 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']); } }
phpuse 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']); } }
phpuse 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']); });
phpuse 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']); });
php$user = User::factory()->state(['role' => 'admin'])->create();
RefreshDatabase 保持干净状态assertDatabaseHas 而非手动查询phpuse 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()phpuse Illuminate\Support\Facades\Queue; Queue::fake(); dispatch(new SendOrderConfirmation($order->id)); Queue::assertPushed(SendOrderConfirmation::class);
phpuse Illuminate\Support\Facades\Notification; Notification::fake(); $user->notify(new InvoiceReady($invoice)); Notification::assertSentTo($user, InvoiceReady::class);
phpuse Laravel\Sanctum\Sanctum; Sanctum::actingAs($user); $response = $this->getJson('/api/projects'); $response->assertOk();
Http::fake() 隔离外部 APIHttp::assertSent() 断言出站负载pcov 或 XDEBUG_MODE=coveragephp artisan testvendor/bin/phpunitvendor/bin/pestphpunit.xml 设置 DB_CONNECTION=sqlite 和 DB_DATABASE=:memory: 以进行快速测试phpuse Illuminate\Support\Facades\Gate; $this->assertTrue(Gate::forUser($user)->allows('update', $project)); $this->assertFalse(Gate::forUser($otherUser)->allows('update', $project));
使用 Inertia.js 时,使用 Inertia 测试辅助函数来断言组件名称和属性。
phpuse 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 响应一致。
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 13,437 | 12,644 | -6% | 1 | 1 | 0% | 3,226 | 4,833 | +50% | 0 | 0 | — |
case-02 | pass→pass | 12,774 | 8,741 | -32% | 1 | 1 | 0% | 2,523 | 3,852 | +53% | 0 | 0 | — |
case-03 | fail→pass | 11,709 | 9,573 | -18% | 1 | 1 | 0% | 2,438 | 4,108 | +68% | 0 | 0 | — |
case-04 | pass→pass | 11,887 | 11,247 | -5% | 1 | 1 | 0% | 2,721 | 4,480 | +65% | 0 | 0 | — |
case-05 | pass→pass | 4,630 | 4,050 | -13% | 1 | 1 | 0% | 958 | 2,772 | +189% | 0 | 0 | — |
case-06 | pass→pass | 17,868 | 14,448 | -19% | 1 | 1 | 0% | 4,417 | 5,752 | +30% | 0 | 0 | — |
case-07 | pass→pass | 11,058 | 6,669 | -40% | 1 | 1 | 0% | 2,596 | 3,361 | +29% | 0 | 0 | — |
case-08 | pass→pass | 11,813 | 6,494 | -45% | 1 | 1 | 0% | 2,288 | 3,206 | +40% | 0 | 0 | — |
case-09 | pass→pass | 10,314 | 10,161 | -1% | 1 | 1 | 0% | 2,289 | 4,192 | +83% | 0 | 0 | — |
case-10 | pass→pass | 6,810 | 7,453 | +9% | 1 | 1 | 0% | 1,362 | 3,567 | +162% | 0 | 0 | — |
case-20 | pass→pass | 5,454 | 2,815 | -48% | 1 | 1 | 0% | 897 | 2,401 | +168% | 0 | 0 | — |
case-11 | pass→pass | 6,724 | 6,288 | -6% | 1 | 1 | 0% | 1,443 | 3,276 | +127% | 0 | 0 | — |
case-12 | pass→pass | 9,721 | 6,780 | -30% | 1 | 1 | 0% | 2,178 | 3,556 | +63% | 0 | 0 | — |
case-13 | pass→pass | 13,831 | 4,225 | -69% | 1 | 1 | 0% | 2,367 | 2,598 | +10% | 0 | 0 | — |
case-14 | fail→pass | 10,270 | 7,965 | -22% | 1 | 1 | 0% | 2,223 | 3,701 | +66% | 0 | 0 | — |
case-19 | pass→pass | 7,579 | 3,497 | -54% | 1 | 1 | 0% | 1,291 | 2,519 | +95% | 0 | 0 | — |
case-15 | pass→pass | 11,241 | 9,734 | -13% | 1 | 1 | 0% | 2,243 | 3,989 | +78% | 0 | 0 | — |
case-16 | pass→pass | 10,725 | 6,736 | -37% | 1 | 1 | 0% | 2,303 | 3,547 | +54% | 0 | 0 | — |
case-17 | pass→pass | 10,190 | 6,951 | -32% | 1 | 1 | 0% | 2,175 | 3,527 | +62% | 0 | 0 | — |
case-18 | pass→pass | 9,841 | 6,047 | -39% | 1 | 1 | 0% | 1,894 | 3,076 | +62% | 0 | 0 | — |
case-21 | pass→pass | 11,666 | 7,174 | -39% | 1 | 1 | 0% | 2,087 | 3,151 | +51% | 0 | 0 | — |
case-22 | pass→pass | 5,698 | 5,842 | +3% | 1 | 1 | 0% | 1,178 | 3,192 | +171% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.