Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Test-driven development for Laravel with PHPUnit and Pest, factories, database testing, fakes, and coverage targets.
.claude/skills/loulanyue-laravel-tdd/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 48% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 6% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 24% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 82% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 130% | 0% |
Test-driven development for Laravel applications using PHPUnit and Pest with 80%+ coverage (unit + feature).
1) Write a failing test 2) Implement the minimal change to pass 3) Refactor while keeping tests green
Choose layers based on scope:
RefreshDatabase for most feature/integration tests (runs migrations once per test run, then wraps each test in a transaction when supported; in-memory databases may re-migrate per test)DatabaseTransactions when the schema is already migrated and you only need per-test rollbackDatabaseMigrations when you need a full migrate/fresh for every test and can afford the costUse RefreshDatabase as the default for tests that touch the database: for databases with transaction support, it runs migrations once per test run (via a static flag) and wraps each test in a transaction; for :memory: SQLite or connections without transactions, it migrates before each test. Use DatabaseTransactions when the schema is already migrated and you only need per-test rollbacks.
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 for clean stateassertDatabaseHas over manual queriesphpuse 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() for jobsQueue::fake() for queued workMail::fake() and Notification::fake() for notificationsEvent::fake() for domain eventsphpuse 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() to isolate external APIsHttp::assertSent()pcov or XDEBUG_MODE=coverage in CIphp artisan testvendor/bin/phpunitvendor/bin/pestphpunit.xml to set DB_CONNECTION=sqlite and DB_DATABASE=:memory: for fast testsphpuse Illuminate\Support\Facades\Gate; $this->assertTrue(Gate::forUser($user)->allows('update', $project)); $this->assertFalse(Gate::forUser($otherUser)->allows('update', $project));
When using Inertia.js, assert on the component name and props with the Inertia testing helpers.
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') ); } }
Prefer assertInertia over raw JSON assertions to keep tests aligned with Inertia responses.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 16,802 | 12,692 | -24% | 1 | 1 | 0% | 3,117 | 4,601 | +48% | 0 | 0 | — |
case-02 | pass→pass | 13,857 | 2,281 | -84% | 1 | 1 | 0% | 2,173 | 2,305 | +6% | 0 | 0 | — |
case-03 | pass→pass | 13,270 | 4,771 | -64% | 1 | 1 | 0% | 2,153 | 2,676 | +24% | 0 | 0 | — |
case-04 | pass→pass | 8,960 | 4,673 | -48% | 1 | 1 | 0% | 1,470 | 2,678 | +82% | 0 | 0 | — |
case-05 | pass→pass | 6,487 | 3,448 | -47% | 1 | 1 | 0% | 1,065 | 2,450 | +130% | 0 | 0 | — |
case-06 | pass→pass | 5,699 | 4,157 | -27% | 1 | 1 | 0% | 1,000 | 2,733 | +173% | 0 | 0 | — |
case-07 | pass→pass | 9,650 | 6,159 | -36% | 1 | 1 | 0% | 1,790 | 3,082 | +72% | 0 | 0 | — |
case-08 | pass→pass | 10,326 | 6,533 | -37% | 1 | 1 | 0% | 1,753 | 3,114 | +78% | 0 | 0 | — |
case-09 | pass→pass | 5,122 | 5,217 | +2% | 1 | 1 | 0% | 908 | 2,921 | +222% | 0 | 0 | — |
case-10 | pass→pass | 10,553 | 6,136 | -42% | 1 | 1 | 0% | 1,959 | 3,200 | +63% | 0 | 0 | — |
case-11 | pass→pass | 5,417 | 3,955 | -27% | 1 | 1 | 0% | 889 | 2,568 | +189% | 0 | 0 | — |
case-12 | pass→pass | 12,302 | 10,145 | -18% | 1 | 1 | 0% | 2,221 | 3,694 | +66% | 0 | 0 | — |
case-13 | pass→pass | 9,973 | 4,358 | -56% | 1 | 1 | 0% | 1,680 | 2,758 | +64% | 0 | 0 | — |
case-14 | pass→pass | 8,766 | 3,780 | -57% | 1 | 1 | 0% | 1,549 | 2,531 | +63% | 0 | 0 | — |
case-15 | pass→pass | 11,208 | 4,244 | -62% | 1 | 1 | 0% | 1,707 | 2,638 | +55% | 0 | 0 | — |
case-16 | pass→pass | 9,565 | 4,254 | -56% | 1 | 1 | 0% | 1,465 | 2,507 | +71% | 0 | 0 | — |
case-17 | pass→pass | 4,722 | 4,287 | -9% | 1 | 1 | 0% | 869 | 2,684 | +209% | 0 | 0 | — |
case-18 | pass→pass | 5,242 | 5,250 | +0% | 1 | 1 | 0% | 921 | 2,671 | +190% | 0 | 0 | — |
case-19 | pass→pass | 10,383 | 4,262 | -59% | 1 | 1 | 0% | 1,673 | 2,678 | +60% | 0 | 0 | — |
case-20 | pass→pass | 5,011 | 4,889 | -2% | 1 | 1 | 0% | 991 | 2,941 | +197% | 0 | 0 | — |
case-21 | pass→pass | 11,873 | 7,812 | -34% | 1 | 1 | 0% | 2,399 | 3,594 | +50% | 0 | 0 | — |
case-22 | pass→pass | 14,447 | 12,121 | -16% | 1 | 1 | 0% | 3,051 | 4,564 | +50% | 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 +5 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.