Skip to content
Back to skills

Laravel Tdd

ASecurity

Drives Laravel feature development with Pest or PHPUnit — factories, RefreshDatabase, fakes for queues/mail/notifications, Sanctum auth, and Inertia assertions. Use when writing or fixing a Laravel controller, Eloquent model, policy, job, or notification, when the project uses Pest/PHPUnit, or when asked to test an Inertia page's props.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 16, 2026
developmentgophpsqltestingapidatabase

Works with

  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 16, 2026

npx -y skills add shennawardana23/skillme --skill laravel-tdd --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Laravel Tdd?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Laravel Tdd
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/shennawardana23-laravel-tdd/badge)](https://www.skillsdirectory.com/skills/shennawardana23-laravel-tdd)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: laravel-tdd
description: Drives Laravel feature development with Pest or PHPUnit — factories, RefreshDatabase, fakes for queues/mail/notifications, Sanctum auth, and Inertia assertions. Use when writing or fixing a Laravel controller, Eloquent model, policy, job, or notification, when the project uses Pest/PHPUnit, or when asked to test an Inertia page's props.
license: Apache-2.0
metadata:
  version: "0.1.0"
---

# Laravel TDD

Red-green-refactor for Laravel, with the framework-specific parts that
differ from a generic TDD loop: which database trait to reach for, which
`Facade::fake()` to use for a given side effect, and how to assert an
Inertia response instead of raw JSON.

For the generic RED-GREEN-REFACTOR cycle and the Prove-It bug-fix pattern,
see `skills/test-driven-development/`. This skill covers what's specific to
Laravel on top of that cycle.

## Test layers

- **Unit** — pure PHP: value objects, calculators, services with no
  framework dependency.
- **Feature** — HTTP endpoints, auth, validation, policies, response shape.
- **Integration** — database + queue + external boundaries together.

Pick the layer by what you need to prove, not by habit: a validation rule
belongs in a Feature test that hits the route (it proves the `FormRequest`
is wired in), not a Unit test that instantiates the rule class directly.

## Framework choice

Default to **Pest** for new test files when the project has it installed;
use **PHPUnit** only if the project already standardizes on it. Don't mix
styles within one test file.

```php
// Pest
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']);
});
```

```php
// PHPUnit — same behavior, class-based
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']);
    }
}
```

## Database trait: which one and why

- **`RefreshDatabase`** — the default for feature/integration tests. On a
  connection with transaction support it migrates once per test run (a
  static flag) and wraps each test in a rolled-back transaction; on
  `:memory:` SQLite (no cross-connection transaction visibility) it
  re-migrates before every test, which is slower.
- **`DatabaseTransactions`** — use when the schema is already migrated
  (e.g. a persistent test database) and you only need per-test rollback,
  skipping the migration-freshness check `RefreshDatabase` does.
- **`DatabaseMigrations`** — full migrate-and-drop every test. Reserve for
  tests that specifically verify migration behavior; it's the slowest
  option and unnecessary for ordinary feature tests.

## Factories and states

Define named states for the edge cases you'll actually test, not just the
happy-path row:

```php
$admin = User::factory()->state(['role' => 'admin'])->create();
$archived = Project::factory()->archived()->create();
```

## Faking side effects

Don't let a test send a real email, dispatch a real job, or hit a real
external API. Match the fake to the side effect:

```php
Queue::fake();
dispatch(new SendOrderConfirmation($order->id));
Queue::assertPushed(SendOrderConfirmation::class);

Notification::fake();
$user->notify(new InvoiceReady($invoice));
Notification::assertSentTo($user, InvoiceReady::class);

Http::fake();
// ... code that calls an external API ...
Http::assertSent(fn ($request) => $request->url() === 'https://api.example.com/charge');
```

`Bus::fake()` for jobs dispatched via `Bus::dispatch`/`dispatchNow`,
`Mail::fake()` for `Mail::send`/`Mailable::send`, `Event::fake()` for
domain events — pick the facade that matches how the code actually
triggers the side effect, not the closest-sounding one.

## Auth in tests

```php
use Laravel\Sanctum\Sanctum;

Sanctum::actingAs($user);
$response = $this->getJson('/api/projects');
$response->assertOk();
```

For policy checks directly (without a full HTTP round trip):

```php
$this->assertTrue(Gate::forUser($user)->allows('update', $project));
$this->assertFalse(Gate::forUser($otherUser)->allows('update', $project));
```

## Inertia responses

Assert on the component name and props, not on raw JSON — Inertia
responses aren't meant to be consumed as a plain API contract:

```php
$response = $this->actingAs($user)->get('/dashboard');

$response->assertInertia(fn (AssertableInertia $page) => $page
    ->component('Dashboard')
    ->where('user.id', $user->id)
    ->has('projects')
);
```

## Coverage and commands

- `php artisan test`, `vendor/bin/pest`, or `vendor/bin/phpunit`.
- Coverage needs `pcov` installed or `XDEBUG_MODE=coverage` set — without
  one of these, `--coverage` silently reports 0% rather than failing loudly.
- Set `DB_CONNECTION=sqlite` / `DB_DATABASE=:memory:` in the test
  environment (`phpunit.xml` or `.env.testing`) for fast, isolated runs
  that can't touch dev/prod data.

## Gotchas

- `RefreshDatabase` on `:memory:` SQLite re-migrates per test — if a suite
  is unexpectedly slow, check whether it's on `:memory:` when it could use
  a file-backed SQLite or a real Postgres/MySQL test database with
  transactions instead.
- Forgetting `Queue::fake()`/`Mail::fake()` doesn't just make a test slow —
  it can dispatch a real email or job during a test run against
  whatever queue/mail driver the test environment happens to have
  configured. Fake before triggering the side effect, not after.
- `assertDatabaseHas` after a mocked/faked repository call proves nothing —
  if the write path is faked, the row was never inserted; assert against
  the fake's own assertion methods (`Queue::assertPushed`, etc.) instead.
- Factory `state()` closures that reference `$this` or another factory's
  result at *definition* time (rather than inside the closure) capture a
  stale value shared across every invocation — keep dynamic data inside
  the closure body.

## Real-world grounding

Laravel's own `RefreshDatabase` trait exists because the older
`DatabaseMigrations` trait (still available) reruns every migration for
every test — for a schema with hundreds of migrations this turns a test
suite from seconds into minutes. The framework's documented recommendation
to default to `RefreshDatabase` and reserve `DatabaseMigrations` for
migration-specific tests is a direct response to that measured cost, not a
stylistic preference.

## Verification

- [ ] A failing test existed before the implementation change (RED)
- [ ] The correct database trait was chosen for the test's actual needs
- [ ] External side effects (queue, mail, notification, HTTP) are faked,
      not real
- [ ] Inertia responses are asserted via `assertInertia`, not raw JSON
- [ ] `php artisan test` (or `vendor/bin/pest`/`phpunit`) passes locally

Files in this skill

  • SKILL.md7 KB
  • evals/evals.json3.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.