Architecture Tests with Pest

+15 Mana ✨

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

php
arch('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

php
arch('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:

php
arch()->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

  1. Using expect() outside arch() thinking it's the same API — the arch matchers (toUse, toBeFinal, etc.) only exist inside arch(). They won't resolve in a regular test.
  2. Forgetting ->ignoring('tests') — you probably want dd() in tests during debugging. Without ignoring, the rule blocks every debugging session.

Best Practices

  1. Start with a preset, then customize — arch()->preset()->laravel() gets you most conventions for free.
  2. 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, toExtend are the common matchers.
  • Presets bundle rule sets for Laravel, PHP core, and security.
  • Run alongside normal tests with php artisan test.
✓ Completed