pest-testing
When to use
Use this skill for all Laravel testing tasks, especially when working with:
- Feature tests
- Unit tests
- API endpoint tests
- Model tests
- Service tests
- Authorization tests
- Validation tests
- Database interaction tests
- Factories, fakes, mocks, and test setup
This skill extends php-coder, laravel, and eloquent.
For prevention layers that fire before writing a test — TDD
discipline, mock-isolation gates, and the 12 process rationalizations
("I'll add the test after", "patch first, test later") — see
test-driven-development,
testing-anti-patterns, and
process-anti-patterns.md.
Procedure: Write Pest tests
- Read the base skills first — apply
php-coder, laravel, and eloquent where relevant.
- Check the project's test framework — confirm Pest is used and inspect existing tests.
- Match the current test style — naming, helpers, datasets, expectations, setup, traits, and folder structure.
- Check available factories and seeders — reuse existing test data patterns.
- Understand the behavior under test — inspect controllers, services, requests, policies, jobs, and models before writing tests.
- Prefer existing helpers — authentication helpers, custom assertions, base test classes, and shared setup.
Core testing principles
- Test behavior, not implementation details.
- Prefer clear, intention-revealing tests over overly clever abstractions.
- One test should verify one meaningful behavior.
- Keep setup minimal and relevant.
- Favor confidence and maintainability over excessive mocking.
- Cover happy path, validation failures, authorization failures, and important edge cases.
- Enumerate the cases BEFORE writing — run the
test-case-discovery funnel; floor per behavior: 1 happy + 1 boundary + 1 error (+1 abuse on security paths).
TDD workflow (Red-Green-Refactor)
For bug fixes and new features, prefer test-driven development:
- RED — Write a failing test that describes the expected behavior.
- Verify RED — Run the test, confirm it fails for the expected reason (missing feature,
not a typo or syntax error). If the test passes immediately, it tests existing behavior — fix it.
- GREEN — Write the minimal code to make the test pass. No extras, no "while I'm here".
- Verify GREEN — Run all tests, confirm the new test passes and nothing else broke.
- REFACTOR — Clean up code while keeping tests green.
Why test-first matters
Tests written after implementation pass immediately. Passing immediately proves nothing:
- The test might test the wrong thing.
- The test might test implementation, not behavior.
- You never saw it catch the bug — so you don't know if it would.
Bug fix TDD
For every bug fix: write a failing test that reproduces the bug FIRST, then fix it.
The test proves the fix works AND prevents regression.
TDD rationalization prevention
| Excuse |
Reality |
| "Too simple to test" |
Simple code breaks. Test takes 30 seconds. |
| "I'll test after" |
Tests passing immediately prove nothing. |
| "Manual test is faster" |
Manual doesn't prevent regression. You'll re-test every change. |
| "Test is hard to write" |
Hard to test = hard to use. Simplify the design. |
| "Need to explore first" |
Fine — throw away exploration code, start fresh with TDD. |
| "Existing code has no tests" |
You're improving it. Add tests for what you touch. |
Laravel testing rules
- Use Feature tests for HTTP endpoints, request validation, middleware behavior, authorization, and end-to-end application flow.
- Use Unit tests for isolated services or pure logic when true isolation adds value.
- Prefer Feature tests over Unit tests when framework integration is part of the behavior.
- Use
RefreshDatabase or the project's standard database reset strategy where appropriate.
- Reuse factories instead of manually creating large fixture arrays.
Pest style rules
- Write descriptive test names in plain language.
- Use
it() / test() according to existing project conventions.
- Group related tests logically.
- Use datasets when they improve readability and reduce duplication.
- Keep each test focused and concise.
Pest-specific PHP rules
- Do NOT use
readonly or final on Pest test classes.
- Do NOT mark classes
final if they need to be mocked via Mockery::mock().
- Pest test files (without a
namespace declaration) treat all PHP built-in classes as global.
Do NOT add use statements for global classes like DateTimeImmutable, Exception,
stdClass, etc. — PHP will warn: "The use statement with non-compound name has no effect".
- Only
use statements for namespaced classes (e.g., use App\Models\...) are needed.
Avoiding flaky tests
- Time-dependent tests: Use
$this->travel(5)->seconds() (Laravel's time travel) to create
a clear gap between "before" and "after" timestamps. Never rely on now() being different
between two lines of code — on fast hardware, they can be identical.
- Database-dependent tests: Don't assume column values are
null just because the seeder
doesn't set them — previous tests in parallel may have modified the same record.
- Parallel testing: The project may use parallel testing (8+ processes). Avoid relying on
global state, specific row counts, or auto-increment IDs.
HTTP and API tests
- Test:
- status codes
- response structure
- validation errors
- authorization behavior
- persistence side effects
- For JSON APIs, assert:
- exact relevant fields
- error structure when applicable
- database state after the request
- Do not only assert
200 — verify meaningful behavior.
Validation tests
- Validate important request rules explicitly.
- Cover required fields, invalid formats, boundary values, and business-critical constraints.
- Prefer focused validation tests over giant "all fields invalid" tests unless the project already uses that pattern.
Authorization tests
- Always test protected actions for:
- guest users
- unauthorized users
- authorized users
- Match the project's auth setup and policy usage.
- Do not assume authorization works just because a policy exists.
Database assertions
- Assert persistence effects with:
assertDatabaseHas
assertDatabaseMissing
- relation checks where relevant
- Keep assertions focused on meaningful fields.
- Do not assert every column unless necessary.
Snapshot testing with coduo/php-matcher
This project uses coduo/php-matcher for flexible snapshot assertions.
Pattern files live in snapshots/ directories next to the test files.
Pattern variables
Use pattern variables instead of hardcoded values in snapshot files.
This makes snapshots resilient to data changes while still enforcing type correctness.
| Pattern |
Matches |
Example |
@boolean@ |
true or false |
'is_active' => '@boolean@' |
@integer@ |
Any integer |
'id' => '@integer@' |
@string@ |
Any string |
'name' => '@string@' |
@null@ |
null |
'deleted_at' => '@null@' |
@datetime@ |
ISO datetime string |
'created_at' => '@datetime@' |
@uuid@ |
UUID string |
'uuid' => '@uuid@' |
@array@ |
Any array |
'items' => '@array@' |
@double@ |
Any float |
'amount' => '@double@' |
@wildcard@ |
Anything |
'data' => '@wildcard@' |
Combine with || for nullable fields: 'deleted_at' => '@null@||@datetime@'
Rules
- Never hardcode dynamic values (IDs, timestamps, UUIDs) in snapshots — use pattern variables or
$replacements.
- Never hardcode boolean defaults (e.g.,
false) when other booleans in the same file use @boolean@ — be consistent.
- Use
$variable ?? '@pattern@' syntax to allow test-specific overrides via replacements parameter.
- Use
PhpMatcherHelper::ruleBackedEnum(EnumClass::class, 'string') for enum fields.
Example snapshot file
// snapshots/user-resource.php
return [
'id' => $id ?? '@integer@',
'name' => $name ?? '@string@',
'email' => $email ?? '@string@',
'is_active' => $is_active ?? '@boolean@',
'created_at' => $created_at ?? '@datetime@',
'deleted_at' => $deleted_at ?? '@null@||@datetime@',
];
Example test usage
expect($response->json())
->toMatchPhpPatternFile(
patternFile: __DIR__ . '/snapshots/user-resource.php',
replacements: ['id' => $user->getId()],
);
Fakes, mocks, and external boundaries
- Use Laravel fakes for framework integrations when appropriate:
Queue::fake()
Bus::fake()
Event::fake()
Mail::fake()
Notification::fake()
Storage::fake()
- Mock only true external boundaries or expensive dependencies.
- Avoid mocking internal application code unless isolation is necessary for the specific test.
Factories and fixtures
- Prefer factories with explicit state over large inline setup.
- Use named states for meaningful scenarios.
- Keep test data realistic and minimal.
- Do not create unnecessary records.
Test quality analysis
When reviewing or auditing existing tests, check for these anti-patterns:
Test smells to detect
| Smell |
Description |
Fix |
| Overmocking |
Too many mocks disconnect the test from reality |
Replace mocks with real implementations or fakes |
| Fragile tests |
Tests break with unrelated changes (e.g., asserting exact JSON structure) |
Assert only meaningful fields |
| Flaky tests |
Non-deterministic results (time, ordering, parallel state) |
Use time travel, explicit ordering, isolated data |
| Giant tests |
One test covers 5+ behaviors |
Split into focused tests |
| Missing assertions |
Test runs code but doesn't verify outcomes |
Add meaningful assertions |
| Test duplication |
Same scenario tested in multiple places |
Consolidate or use datasets |
| Assertion roulette |
Many assertions without clear failure messages |
Use named assertions or split tests |
| Eager test |
Tests too many things, making failures hard to diagnose |
One behavior per test |
FIRST principles
- Fast — Tests should run quickly. Avoid unnecessary DB operations.
- Isolated — Tests should not depend on each other or shared state.
- Repeatable — Same result every time, regardless of environment or order.
- Self-validating — Pass or fail, no manual inspection needed.
- Timely — Written close to the code they test.
Mock usage guidelines
- Mock external boundaries (APIs, file systems, third-party services).
- Use Laravel fakes (
Queue::fake(), Http::fake()) over manual mocks.
- Do NOT mock the class under test.
- Do NOT mock value objects or DTOs.
- If a test needs 3+ mocks, consider testing at a higher level (Feature test).
What NOT to do
- Do not test private methods directly.
- Do not over-mock Laravel internals.
- Do not assert implementation details when behavior assertions are enough.
- Do not write brittle tests tied to formatting or irrelevant response noise.
- Do not create giant tests that cover many behaviors at once.
- Do not skip authorization or validation coverage for important endpoints.
Output expectations
When generating Pest tests:
- follow the existing folder and naming conventions
- test behavior clearly and directly
- cover success, failure, and authorization paths
- use factories and Laravel test helpers
- assert both response and side effects where relevant
- keep tests readable, isolated, and maintainable
Filter Pest output
When triaging a verbose run, narrow the output with --filter (Pest)
plus targeted grep/rg instead of re-running the whole suite or
echoing all logs:
# Only run failing tests in one file
vendor/bin/pest tests/Feature/InvoiceTest.php --filter='creates invoice'
# Scan the test log for failures only
rg --color=never '^FAIL|Tests:' storage/logs/pest.log
# Inspect JSON output from data-driven tests
vendor/bin/pest --log-junit=pest.xml && rg '<failure' pest.xml
Output format
- Pest test file with descriptive test names and clear assertions
- Tests organized by happy path, validation, edge cases
Gotcha
- Don't use
readonly or final on Pest test helper classes — it breaks mocking.
- Don't add
use statements for global classes (Exception, DateTimeImmutable) in Pest files — they're auto-imported.
- The model forgets
$this->travel(5)->seconds() for time-dependent tests — never rely on now() differing between lines.
- Parallel tests share the database — don't assume column values are null unless you explicitly set them.
Do NOT
- Do NOT mark classes final if they need to be mocked via Mockery.
- Do NOT use PHPUnit class-based syntax — use Pest syntax.
What to test (generation checklist)
When generating new tests, focus on:
- Business logic: calculations, status transitions, validation rules, data transformations
- Edge cases: null, empty string, zero, negative numbers, boundary values, max length
- Error paths: invalid input, missing dependencies, exception handling
- Different code branches: if/else, early returns, fallback behavior
What NOT to test:
- Trivial getters/setters without logic
- Parameter counts, method existence, class names
- Framework internals (Eloquent, routing)
- Private methods directly — test through public API
Quality over quantity — 5 meaningful tests beat 20 trivial ones.
Auto-trigger keywords
- Pest test
- PHPUnit
- test writing
- test quality
- TDD
- generate tests
- write tests
- test coverage
- test scenarios
1---2name: pest-testing3description: Use when writing, generating, or improving Pest tests for Laravel — clear intent, good coverage, maintainable structure, and alignment with project testing conventions.4---56# pest-testing78## When to use910Use this skill for all Laravel testing tasks, especially when working with:1112- Feature tests13- Unit tests14- API endpoint tests15- Model tests16- Service tests17- Authorization tests18- Validation tests19- Database interaction tests20- Factories, fakes, mocks, and test setup2122This skill extends `php-coder`, `laravel`, and `eloquent`.2324For prevention layers that fire **before** writing a test — TDD25discipline, mock-isolation gates, and the 12 process rationalizations26("I'll add the test after", "patch first, test later") — see27[`test-driven-development`](../test-driven-development/SKILL.md),28[`testing-anti-patterns`](../testing-anti-patterns/SKILL.md), and29[`process-anti-patterns.md`](../testing-anti-patterns/process-anti-patterns.md).3031## Procedure: Write Pest tests32331. **Read the base skills first** — apply `php-coder`, `laravel`, and `eloquent` where relevant.342. **Check the project's test framework** — confirm Pest is used and inspect existing tests.353. **Match the current test style** — naming, helpers, datasets, expectations, setup, traits, and folder structure.364. **Check available factories and seeders** — reuse existing test data patterns.375. **Understand the behavior under test** — inspect controllers, services, requests, policies, jobs, and models before writing tests.386. **Prefer existing helpers** — authentication helpers, custom assertions, base test classes, and shared setup.3940## Core testing principles4142- Test behavior, not implementation details.43- Prefer clear, intention-revealing tests over overly clever abstractions.44- One test should verify one meaningful behavior.45- Keep setup minimal and relevant.46- Favor confidence and maintainability over excessive mocking.47- Cover happy path, validation failures, authorization failures, and important edge cases.48- Enumerate the cases BEFORE writing — run the [`test-case-discovery`](../test-case-discovery/SKILL.md) funnel; floor per behavior: 1 happy + 1 boundary + 1 error (+1 abuse on security paths).4950## TDD workflow (Red-Green-Refactor)5152For bug fixes and new features, prefer test-driven development:53541. **RED** — Write a failing test that describes the expected behavior.552. **Verify RED** — Run the test, confirm it fails for the expected reason (missing feature,56 not a typo or syntax error). If the test passes immediately, it tests existing behavior — fix it.573. **GREEN** — Write the **minimal** code to make the test pass. No extras, no "while I'm here".584. **Verify GREEN** — Run all tests, confirm the new test passes and nothing else broke.595. **REFACTOR** — Clean up code while keeping tests green.6061### Why test-first matters6263Tests written **after** implementation pass immediately. Passing immediately proves nothing:64- The test might test the wrong thing.65- The test might test implementation, not behavior.66- You never saw it catch the bug — so you don't know if it would.6768### Bug fix TDD6970For every bug fix: write a failing test that reproduces the bug FIRST, then fix it.71The test proves the fix works AND prevents regression.7273### TDD rationalization prevention7475| Excuse | Reality |76|---|---|77| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |78| "I'll test after" | Tests passing immediately prove nothing. |79| "Manual test is faster" | Manual doesn't prevent regression. You'll re-test every change. |80| "Test is hard to write" | Hard to test = hard to use. Simplify the design. |81| "Need to explore first" | Fine — throw away exploration code, start fresh with TDD. |82| "Existing code has no tests" | You're improving it. Add tests for what you touch. |8384## Laravel testing rules8586- Use **Feature tests** for HTTP endpoints, request validation, middleware behavior, authorization, and end-to-end application flow.87- Use **Unit tests** for isolated services or pure logic when true isolation adds value.88- Prefer Feature tests over Unit tests when framework integration is part of the behavior.89- Use `RefreshDatabase` or the project's standard database reset strategy where appropriate.90- Reuse factories instead of manually creating large fixture arrays.9192## Pest style rules9394- Write descriptive test names in plain language.95- Use `it()` / `test()` according to existing project conventions.96- Group related tests logically.97- Use datasets when they improve readability and reduce duplication.98- Keep each test focused and concise.99100## Pest-specific PHP rules101102- Do NOT use `readonly` or `final` on Pest test classes.103- Do NOT mark classes `final` if they need to be mocked via `Mockery::mock()`.104- Pest test files (without a `namespace` declaration) treat all PHP built-in classes as global.105 Do **NOT** add `use` statements for global classes like `DateTimeImmutable`, `Exception`,106 `stdClass`, etc. — PHP will warn: *"The use statement with non-compound name has no effect"*.107- Only `use` statements for **namespaced** classes (e.g., `use App\Models\...`) are needed.108109## Avoiding flaky tests110111- **Time-dependent tests:** Use `$this->travel(5)->seconds()` (Laravel's time travel) to create112 a clear gap between "before" and "after" timestamps. Never rely on `now()` being different113 between two lines of code — on fast hardware, they can be identical.114- **Database-dependent tests:** Don't assume column values are `null` just because the seeder115 doesn't set them — previous tests in parallel may have modified the same record.116- **Parallel testing:** The project may use parallel testing (8+ processes). Avoid relying on117 global state, specific row counts, or auto-increment IDs.118119## HTTP and API tests120121- Test:122 - status codes123 - response structure124 - validation errors125 - authorization behavior126 - persistence side effects127- For JSON APIs, assert:128 - exact relevant fields129 - error structure when applicable130 - database state after the request131- Do not only assert `200` — verify meaningful behavior.132133## Validation tests134135- Validate important request rules explicitly.136- Cover required fields, invalid formats, boundary values, and business-critical constraints.137- Prefer focused validation tests over giant "all fields invalid" tests unless the project already uses that pattern.138139## Authorization tests140141- Always test protected actions for:142 - guest users143 - unauthorized users144 - authorized users145- Match the project's auth setup and policy usage.146- Do not assume authorization works just because a policy exists.147148## Database assertions149150- Assert persistence effects with:151 - `assertDatabaseHas`152 - `assertDatabaseMissing`153 - relation checks where relevant154- Keep assertions focused on meaningful fields.155- Do not assert every column unless necessary.156157## Snapshot testing with `coduo/php-matcher`158159This project uses [`coduo/php-matcher`](https://github.com/coduo/php-matcher) for flexible snapshot assertions.160Pattern files live in `snapshots/` directories next to the test files.161162### Pattern variables163164Use pattern variables instead of hardcoded values in snapshot files.165This makes snapshots resilient to data changes while still enforcing type correctness.166167| Pattern | Matches | Example |168|---|---|---|169| `@boolean@` | `true` or `false` | `'is_active' => '@boolean@'` |170| `@integer@` | Any integer | `'id' => '@integer@'` |171| `@string@` | Any string | `'name' => '@string@'` |172| `@null@` | `null` | `'deleted_at' => '@null@'` |173| `@datetime@` | ISO datetime string | `'created_at' => '@datetime@'` |174| `@uuid@` | UUID string | `'uuid' => '@uuid@'` |175| `@array@` | Any array | `'items' => '@array@'` |176| `@double@` | Any float | `'amount' => '@double@'` |177| `@wildcard@` | Anything | `'data' => '@wildcard@'` |178179Combine with `||` for nullable fields: `'deleted_at' => '@null@||@datetime@'`180181### Rules182183- **Never hardcode dynamic values** (IDs, timestamps, UUIDs) in snapshots — use pattern variables or `$replacements`.184- **Never hardcode boolean defaults** (e.g., `false`) when other booleans in the same file use `@boolean@` — be consistent.185- Use `$variable ?? '@pattern@'` syntax to allow test-specific overrides via `replacements` parameter.186- Use `PhpMatcherHelper::ruleBackedEnum(EnumClass::class, 'string')` for enum fields.187188### Example snapshot file189190```php191// snapshots/user-resource.php192return [193 'id' => $id ?? '@integer@',194 'name' => $name ?? '@string@',195 'email' => $email ?? '@string@',196 'is_active' => $is_active ?? '@boolean@',197 'created_at' => $created_at ?? '@datetime@',198 'deleted_at' => $deleted_at ?? '@null@||@datetime@',199];200```201202### Example test usage203204```php205expect($response->json())206 ->toMatchPhpPatternFile(207 patternFile: __DIR__ . '/snapshots/user-resource.php',208 replacements: ['id' => $user->getId()],209 );210```211212## Fakes, mocks, and external boundaries213214- Use Laravel fakes for framework integrations when appropriate:215 - `Queue::fake()`216 - `Bus::fake()`217 - `Event::fake()`218 - `Mail::fake()`219 - `Notification::fake()`220 - `Storage::fake()`221- Mock only true external boundaries or expensive dependencies.222- Avoid mocking internal application code unless isolation is necessary for the specific test.223224## Factories and fixtures225226- Prefer factories with explicit state over large inline setup.227- Use named states for meaningful scenarios.228- Keep test data realistic and minimal.229- Do not create unnecessary records.230231## Test quality analysis232233When reviewing or auditing existing tests, check for these anti-patterns:234235### Test smells to detect236237| Smell | Description | Fix |238|---|---|---|239| **Overmocking** | Too many mocks disconnect the test from reality | Replace mocks with real implementations or fakes |240| **Fragile tests** | Tests break with unrelated changes (e.g., asserting exact JSON structure) | Assert only meaningful fields |241| **Flaky tests** | Non-deterministic results (time, ordering, parallel state) | Use time travel, explicit ordering, isolated data |242| **Giant tests** | One test covers 5+ behaviors | Split into focused tests |243| **Missing assertions** | Test runs code but doesn't verify outcomes | Add meaningful assertions |244| **Test duplication** | Same scenario tested in multiple places | Consolidate or use datasets |245| **Assertion roulette** | Many assertions without clear failure messages | Use named assertions or split tests |246| **Eager test** | Tests too many things, making failures hard to diagnose | One behavior per test |247248### FIRST principles249250- **Fast** — Tests should run quickly. Avoid unnecessary DB operations.251- **Isolated** — Tests should not depend on each other or shared state.252- **Repeatable** — Same result every time, regardless of environment or order.253- **Self-validating** — Pass or fail, no manual inspection needed.254- **Timely** — Written close to the code they test.255256### Mock usage guidelines257258- Mock **external boundaries** (APIs, file systems, third-party services).259- Use Laravel fakes (`Queue::fake()`, `Http::fake()`) over manual mocks.260- Do NOT mock the class under test.261- Do NOT mock value objects or DTOs.262- If a test needs 3+ mocks, consider testing at a higher level (Feature test).263264## What NOT to do265266- Do not test private methods directly.267- Do not over-mock Laravel internals.268- Do not assert implementation details when behavior assertions are enough.269- Do not write brittle tests tied to formatting or irrelevant response noise.270- Do not create giant tests that cover many behaviors at once.271- Do not skip authorization or validation coverage for important endpoints.272273## Output expectations274275When generating Pest tests:276277- follow the existing folder and naming conventions278- test behavior clearly and directly279- cover success, failure, and authorization paths280- use factories and Laravel test helpers281- assert both response and side effects where relevant282- keep tests readable, isolated, and maintainable283284### Filter Pest output285286When triaging a verbose run, narrow the output with `--filter` (Pest)287plus targeted `grep`/`rg` instead of re-running the whole suite or288echoing all logs:289290```bash291# Only run failing tests in one file292vendor/bin/pest tests/Feature/InvoiceTest.php --filter='creates invoice'293294# Scan the test log for failures only295rg --color=never '^FAIL|Tests:' storage/logs/pest.log296297# Inspect JSON output from data-driven tests298vendor/bin/pest --log-junit=pest.xml && rg '<failure' pest.xml299```300301302## Output format3033041. Pest test file with descriptive test names and clear assertions3052. Tests organized by happy path, validation, edge cases306307## Gotcha308309- Don't use `readonly` or `final` on Pest test helper classes — it breaks mocking.310- Don't add `use` statements for global classes (`Exception`, `DateTimeImmutable`) in Pest files — they're auto-imported.311- The model forgets `$this->travel(5)->seconds()` for time-dependent tests — never rely on `now()` differing between lines.312- Parallel tests share the database — don't assume column values are null unless you explicitly set them.313314## Do NOT315316- Do NOT mark classes final if they need to be mocked via Mockery.317- Do NOT use PHPUnit class-based syntax — use Pest syntax.318319## What to test (generation checklist)320321When generating new tests, focus on:322- **Business logic**: calculations, status transitions, validation rules, data transformations323- **Edge cases**: null, empty string, zero, negative numbers, boundary values, max length324- **Error paths**: invalid input, missing dependencies, exception handling325- **Different code branches**: if/else, early returns, fallback behavior326327What NOT to test:328- Trivial getters/setters without logic329- Parameter counts, method existence, class names330- Framework internals (Eloquent, routing)331- Private methods directly — test through public API332333**Quality over quantity** — 5 meaningful tests beat 20 trivial ones.334335## Auto-trigger keywords336337- Pest test338- PHPUnit339- test writing340- test quality341- TDD342- generate tests343- write tests344- test coverage345- test scenarios