Skill
·
v1.0.0
·
MIT
laravel-testing
Test Laravel applications effectively with Pest or PHPUnit - feature vs unit boundaries, factories, database strategy, what to fake, and HTTP/Livewire assertions. Use when writing tests for Laravel code, reviewing a Laravel test suite, deciding how to test a controller, job, or command, or when tests are slow or brittle.
elyra ›
/skills install laravel-testing
In Laravel, the feature test is the workhorse: hit the route, assert the response and the database. Unit tests are for real logic, not for ceremony.
When to use
- Writing tests for Laravel controllers, jobs, commands, actions, or models
- Reviewing a Laravel test suite (coverage, speed, brittleness)
- "How should I test this?" for anything framework-coupled
- Tests are slow, flaky, or break on every refactor
Principles
- Test through the front door.
$this->post(route(...))exercises routing, middleware, validation, authorization, and persistence in one honest test. - Fake the edges, not the middle.
Mail::fake(),Queue::fake(),Http::fake(),Storage::fake()— but don't mock your own repositories and services into a tautology. - Factories are the vocabulary. States (
->suspended(),->paid()) make tests read like specifications. - Each test = one behavior. Arrange, act, assert — and assert the outcome (DB row, response, dispatched job), not the implementation path.
Process
1. Choose the test type
| Code under test | Type |
|---|---|
| Route/controller behavior, auth, validation | Feature (HTTP test) |
| Pure logic: calculators, value objects, rules | Unit (no framework boot if possible) |
| Queued job | Feature-style: instantiate, handle(), assert outcomes |
| Artisan command | $this->artisan(...)->assertExitCode(0) + DB asserts |
| Livewire component | Livewire::test(...) — state, actions, events, validation |
| Blade/UI flows across pages | Few, high-value browser tests (Dusk/Pest browser) |
2. Set the database strategy
RefreshDatabase+ SQLite:memory:for speed — unless you use DB-specific features (JSON columns, full-text, triggers); then test against the real engine, matching productionLazilyRefreshDatabaseskips migration cost for tests that never touch the DB
3. Build with factories
$user = User::factory()->create();
$order = Order::factory()->for($user)->paid()->create(); // states > attribute soup
- Define states in the factory for every domain status the tests mention
make()when persistence isn't needed;create()only when the test reads it back- Never seed global fixtures the whole suite shares — each test builds its own world
4. Write the feature test
it('refunds a paid order', function () {
Queue::fake();
$order = Order::factory()->paid()->create();
$this->actingAs($order->user)
->post(route('orders.refund', $order))
->assertRedirect()
->assertSessionHas('status');
expect($order->fresh()->status)->toBe(OrderStatus::Refunded);
Queue::assertPushed(ProcessRefund::class, fn ($job) => $job->orderId === $order->id);
});
Cover per endpoint: happy path, validation failure (assertSessionHasErrors / assertJsonValidationErrors), authorization (wrong user → 403), and the relevant edge case. Auth tests are the ones that prevent incidents.
5. Fake the boundaries
Http::fake([...])with per-URL responses;Http::assertSent(...)for contract checks; addHttp::preventStrayRequests()suite-wideTime:$this->travel(3)->days()/Carbon::setTestNow()for anything date-dependentEvent::fake()narrowly (Event::fake([OrderShipped::class])) — global fakes silence model events your code depends on
6. Keep it fast and honest
php artisan test --parallel # paratest
php artisan test --profile # find the slow ones
- Suite target: minutes, not tens of minutes; single test: milliseconds unless it's a browser test
- A test that breaks when you rename a private method tests the wrong thing — assert outcomes
Output format
## Tests: <feature/area>
**Type mix:** N feature / M unit / K browser — why.
**DB strategy:** RefreshDatabase + <engine>.
### Coverage
| Behavior | Test |
|----------|------|
| happy path | ✅ … |
| validation | ✅ … |
| authorization | ✅ … |
| <edge case> | ✅ … |
**Faked:** Queue, Http (+preventStrayRequests). **Deliberately real:** …
Anti-patterns
- ❌ Mocking Eloquent models or your own services so the test asserts the mock, not the behavior
- ❌ Unit-testing a controller with mocked request/response instead of one feature test
- ❌ Skipping authorization tests — the most expensive gap in any Laravel suite
- ❌ Shared seeders creating hidden coupling between tests
- ❌ SQLite in tests + MySQL JSON queries in production = green suite, broken prod
- ❌
Event::fake()globally, then wondering why observers "don't work" in tests - ❌ Asserting
assertStatus(200)and nothing else — the page can be 200 and wrong