Skill: Craft Test Suite
"A test suite that requires setup instructions has already failed."
The Standard
Pest + Orchestra Testbench: Pest v3+ for tests (v3 requires PHP 8.2+), Orchestra Testbench for the Laravel environment. Never PHPUnit directly. Prefer Pest's
mock()andspy()helpers over raw Mockery — they wrap Mockery with cleaner syntax.TestCase anatomy: Extend
Orchestra\Testbench\TestCase. OverridegetPackageProviders(),getEnvironmentSetUp(), andsetUpDatabase(). This is the foundation for every package test suite.Pest.phpconfiguration: Bind TestCase globally withuses(TestCase::class)->in(__DIR__). Define custom expectations and helper functions here. One file, one source of truth.it()closures preferred: Preferit()overtest()for natural language readability:it('can create a role'),it('throws when filter not allowed'). Prefer flat file grouping overdescribe()blocks — one concern per file, no nesting.expect()chains:expect($value)->toBe($expected). Never$this->assert*(). One assertion style across the entire suite.Domain-organized directories: Organize by concern, not by layer.
tests/Commands/,tests/Models/,tests/Middleware/. NeverUnit/Feature/Integration.SQLite in-memory: Default database for all tests. Works everywhere, no Docker, no setup. Real database services only when dialect matters.
TestSupport/ directory: Test models, helpers, fixtures, resources. Keep test infrastructure separate from test assertions.
::fake()methods on package facades: Ship fakes so consumers can test package interactions without mocking internals. Follow theBus::fake()contract: record calls, expose assertion methods. See craft-fake for the full pattern.::test()fluent fakes (Livewire): When the package ships Livewire components, useLivewire::test()for fluent interaction testing. The test creates the component, chains interactions, and asserts state in a single call.Architecture tests: Minimum baseline -- no debugging functions. Layer architectural presets for deeper enforcement: strict types, final classes, dependency boundaries. Use Pest's
arch()function with->expect()chains.Higher-order expectations: Use
->eachfor collection item assertions. Use property drilling (expect($user)->name->toBe('Nuno')) for readable nested assertions. Use->sequence()for ordered collection assertions.Custom expectations: Domain-specific assertions are a feature. Register in
Pest.phpviaexpect()->extend(). Ship them in a publishable test helper file so consumers can import and reuse them.Composer scripts:
test,test-coverage,format,analyse. Every package ships with the same four commands. No guessing.Mutation testing:
pest --mutateverifies your tests catch real bugs, not just cover lines. A test that passes on mutated code is a test that proves nothing. Run with--parallelfor speed. Target critical domain logic, not boilerplate.Type coverage:
pest --type-coverage --min=100enforces that every function parameter, return type, and property has a type declaration. No tests required -- it's static analysis. Ship fully typed packages.Parallel testing:
pest --parallelruns tests across CPU cores. Use--processes=Nto control concurrency. Requires test isolation -- no shared database state, no execution-order dependencies.
The Anti-Patterns
| Don't | Do | Why |
|---|---|---|
| PHPUnit directly | Pest | Pest wraps PHPUnit with better DX |
Unit/Feature/Integration dirs |
Domain directories (Commands/, Models/) |
Organize by what, not by how |
$this->assertEquals() |
expect()->toBe() |
One assertion API, not two |
test('it can...') |
it('can...') |
Style preference: natural language, no redundant prefix |
describe() blocks |
Flat file grouping | Style preference: one concern per file, no nesting |
| Real database by default | SQLite in-memory | Zero config, instant feedback |
| Helpers scattered in tests | TestSupport/ directory |
Infrastructure separate from assertions |
setUp() in test files |
beforeEach() |
Pest convention, not PHPUnit ceremony |
| Force consumers to mock internals | Ship ::fake() on the facade |
Mocks couple to implementation, fakes couple to behavior |
| Skip architecture tests | Start with the baseline, layer up | Architecture rot is silent until it isn't |
| 100% code coverage, 0% confidence | --mutate on critical paths |
Mutation testing proves tests catch real bugs |
| Untyped public APIs | --type-coverage --min=100 |
Typed code is self-documenting and IDE-friendly |
| Sequential test runs on CI | --parallel in CI |
Faster feedback, better resource utilization |
Real-World Examples
See examples.md.