Introduction
Architecture tests are Pest's unique feature: assertions about your codebase that run as tests. Instead of relying on code review to catch "services shouldn't use facades" or "DTOs should be final", you write the rule once and CI enforces it forever.
Key Concepts
arch('name', ...): Declares an architecture assertion.->expect('Namespace\\...'): Targets a namespace or list of classes/functions.- Presets: Bundles of common rules (
->preset()->laravel(),->preset()->security()).
Real World Context
A team standard says "services must not use facades — inject dependencies instead." Enforcing that via code review is error-prone; enforcing it via a Pest arch test means the CI fails the moment someone violates it.
Deep Dive
Banning dependencies
php// tests/Arch/ConventionsTest.php arch('services cannot use facades') ->expect('App\\Services') ->not->toUse([ 'Illuminate\\Support\\Facades\\DB', 'Illuminate\\Support\\Facades\\Cache', 'Illuminate\\Support\\Facades\\Http', ]);
Any class in App\Services\* that imports one of those facades fails the test. The fix is to inject the underlying contract via the constructor instead.
Enforcing class shape
phparch('DTOs are final and readonly') ->expect('App\\DTOs') ->toBeFinal() ->toBeReadonly(); arch('controllers end in Controller') ->expect('App\\Http\\Controllers') ->toHaveSuffix('Controller'); arch('models extend Eloquent') ->expect('App\\Models') ->toExtend('Illuminate\\Database\\Eloquent\\Model');
Each of these catches a specific kind of drift that's easy to miss during a rushed PR.
Banning debug leftovers
phparch('no debug calls in production code') ->expect(['dd', 'dump', 'ray', 'var_dump']) ->not->toBeUsed() ->ignoring('tests');
Fires the instant someone commits a stray dd() call. Saves you from the "why is the production page blank?" incident.
Presets
Pest ships presets that bundle common arch rules:
phparch()->preset()->laravel(); // Laravel conventions arch()->preset()->php(); // PHP core standards arch()->preset()->security(); // Dangerous function detection
One line, dozens of checks. Start with these, then layer project-specific rules on top.
Common Pitfalls
- Using
expect()outsidearch()thinking it's the same API — the arch matchers (toUse,toBeFinal, etc.) only exist insidearch(). They won't resolve in a regular test. - Forgetting
->ignoring('tests')— you probably wantdd()in tests during debugging. Without ignoring, the rule blocks every debugging session.
Best Practices
- Start with a preset, then customize —
arch()->preset()->laravel()gets you most conventions for free. - Keep arch tests in a dedicated folder — they're a different kind of test (fast, no setup) and benefit from being discoverable as a group.
Summary
arch()tests assert rules about your codebase: structure, naming, allowed dependencies.toUse,not->toUse,toBeFinal,toBeReadonly,toHaveSuffix,toExtendare the common matchers.- Presets bundle rule sets for Laravel, PHP core, and security.
- Run alongside normal tests with
php artisan test.