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.From its SKILL.md
npx -y skills add kwhorne/elyra-skills --skill laravel-testingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
5.2 KB, ~1.2k tokens by cl100k_base, as published. Nobody here has run it
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
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.