Pest Architecture Tests for Clean Laravel Apps | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com) [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles  ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

  [ home ](https://www.msaied.com)    [ articles ](https://www.msaied.com/articles)    Pest Architecture Tests: Enforcing Clean Boundaries in Laravel Without a CI Nanny        On this page       1. [  Why Architecture Tests Belong in Your Suite ](#why-architecture-tests-belong-in-your-suite)
2. [  Setting Up the Arch Preset ](#setting-up-the-arch-preset)
3. [  Encoding Your Own Layer Rules ](#encoding-your-own-layer-rules)
4. [  Controllers Must Not Touch Models Directly ](#controllers-must-not-touch-models-directly)
5. [  Services Must Not Know About HTTP ](#services-must-not-know-about-http)
6. [  Actions Are Final and Have a Single Public Method ](#actions-are-final-and-have-a-single-public-method)
7. [  Ignoring Specific Classes ](#ignoring-specific-classes)
8. [  Testing That Interfaces Are Properly Implemented ](#testing-that-interfaces-are-properly-implemented)
9. [  Combining With Mutation Testing ](#combining-with-mutation-testing)
10. [  Takeaways ](#takeaways)

  ![Pest Architecture Tests: Enforcing Clean Boundaries in Laravel Without a CI Nanny](https://cdn.msaied.com/166/2747407f8fcc1af6734aa1b0565dc371.png)

  #pest   #laravel   #testing   #clean-architecture   #php  

 Pest Architecture Tests: Enforcing Clean Boundaries in Laravel Without a CI Nanny 
===================================================================================

     14 Jun 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

  10 sections  

1. [  01   Why Architecture Tests Belong in Your Suite  ](#why-architecture-tests-belong-in-your-suite)
2. [  02   Setting Up the Arch Preset  ](#setting-up-the-arch-preset)
3. [  03   Encoding Your Own Layer Rules  ](#encoding-your-own-layer-rules)
4. [  04   Controllers Must Not Touch Models Directly  ](#controllers-must-not-touch-models-directly)
5. [  05   Services Must Not Know About HTTP  ](#services-must-not-know-about-http)
6. [  06   Actions Are Final and Have a Single Public Method  ](#actions-are-final-and-have-a-single-public-method)
7. [  07   Ignoring Specific Classes  ](#ignoring-specific-classes)
8. [  08   Testing That Interfaces Are Properly Implemented  ](#testing-that-interfaces-are-properly-implemented)
9. [  09   Combining With Mutation Testing  ](#combining-with-mutation-testing)
10. [  10   Takeaways  ](#takeaways)

       Why Architecture Tests Belong in Your Suite
-------------------------------------------

Code reviews catch style drift. Architecture tests catch it automatically, every push. Pest ships a first-class `arch()` API that lets you assert structural rules — no external tools, no separate linting step. If a junior dev imports an Eloquent model directly into a Controller that's supposed to go through a Service, the test suite tells them immediately.

This article focuses on practical, opinionated rules for a Laravel project that follows a loose clean-architecture split: `App\Models`, `App\Services`, `App\Http\Controllers`, `App\Actions`, and `App\Repositories`.

---

Setting Up the Arch Preset
--------------------------

Pest ships a Laravel preset that covers the most common rules out of the box:

```php
// tests/ArchTest.php
arch()->preset()->laravel();

```

This single line enforces things like: controllers are not invokable by default, models extend `Illuminate\Database\Eloquent\Model`, jobs are `final`, etc. Run it and fix the noise first — then layer your own rules on top.

---

Encoding Your Own Layer Rules
-----------------------------

### Controllers Must Not Touch Models Directly

```php
arch('controllers do not import models')
    ->expect('App\Http\Controllers')
    ->not->toUse('App\Models');

```

This forces all Eloquent access through Services or Repositories. The moment a controller calls `User::find()` directly, the suite fails.

### Services Must Not Know About HTTP

```php
arch('services are HTTP-agnostic')
    ->expect('App\Services')
    ->not->toUse([
        'Illuminate\Http\Request',
        'Illuminate\Http\Response',
    ]);

```

Services that accept `Request` objects are a common smell. This rule makes it structural policy.

### Actions Are Final and Have a Single Public Method

```php
arch('actions are final')
    ->expect('App\Actions')
    ->toBeFinal();

arch('actions expose only handle')
    ->expect('App\Actions')
    ->toHaveMethod('handle');

```

Combining `toBeFinal()` with a method-name convention keeps single-responsibility honest.

---

Ignoring Specific Classes
-------------------------

Sometimes a rule has one legitimate exception. Use `ignoring()` rather than weakening the rule:

```php
arch('controllers do not import models')
    ->expect('App\Http\Controllers')
    ->not->toUse('App\Models')
    ->ignoring('App\Http\Controllers\Webhooks\StripeController');

```

The exception is explicit and reviewable in git history.

---

Testing That Interfaces Are Properly Implemented
------------------------------------------------

```php
arch('repositories implement contract')
    ->expect('App\Repositories')
    ->toImplement('App\Contracts\RepositoryInterface');

```

If someone adds a repository and forgets the interface, the test fails before the PR is opened.

---

Combining With Mutation Testing
-------------------------------

Arch tests are cheap — they run in milliseconds and require zero database setup. Put them in a dedicated `ArchTest.php` file and exclude them from mutation testing (they have no logic to mutate):

```bash
./vendor/bin/pest --mutate --exclude-testsuite=Architecture

```

Keep your mutation score meaningful by not letting Pest try to mutate structural assertions.

---

Takeaways
---------

- `arch()->preset()->laravel()` gives you sensible defaults for free — start there.
- Encode layer boundaries as tests, not wiki pages nobody reads.
- Use `ignoring()` for exceptions; never weaken the rule itself.
- Arch tests are millisecond-fast — there's no excuse not to run them on every push.
- Combine with Pest's `toBeReadonly()`, `toBeFinal()`, and `toHaveMethod()` for fine-grained control.
- Keep arch tests in a separate file and exclude them from mutation runs.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fpest-architecture-tests-enforcing-clean-boundaries-in-laravel-without-a-ci-nanny&text=Pest+Architecture+Tests%3A+Enforcing+Clean+Boundaries+in+Laravel+Without+a+CI+Nanny) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fpest-architecture-tests-enforcing-clean-boundaries-in-laravel-without-a-ci-nanny) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Do Pest architecture tests require any extra packages?        No. The `arch()` API ships with Pest itself. As long as you have Pest installed in your Laravel project, you can start writing architecture tests immediately with no additional dependencies. 

      Q02  Will architecture tests slow down my CI pipeline?        Barely. Pest resolves architecture rules through static analysis of your class files — no database, no HTTP, no bootstrapping. A full set of arch assertions typically adds under a second to a suite. 

      Q03  What's the difference between the built-in Laravel preset and custom arch rules?        The preset covers generic Laravel conventions (model base class, job finality, etc.). Custom rules encode your project's specific layer contracts — things only your team has agreed on. Use both together. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://www.msaied.com/articles) 

 [ ![Cursor Pagination at Scale with Laravel Lazy Collections and Chunked Iteration](https://cdn.msaied.com/168/72d002a6bc12d3a7278c39e54b7ce46a.png) laravel eloquent performance 

### Cursor Pagination at Scale with Laravel Lazy Collections and Chunked Iteration

Offset pagination collapses under large datasets. Learn how to combine cursor pagination, lazy collections, an...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 14 Jun 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/cursor-pagination-at-scale-with-laravel-lazy-collections-and-chunked-iteration) [ ![Job Batching, Chaining, and Rate-Limited Middleware in Laravel Queues](https://cdn.msaied.com/167/07cf94b1aa8fcead662b9cd5af47acb6.png) laravel queues jobs 

### Job Batching, Chaining, and Rate-Limited Middleware in Laravel Queues

Go beyond basic dispatching: learn how to compose Laravel job batches, build reliable chains, and throttle thr...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 14 Jun 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/job-batching-chaining-and-rate-limited-middleware-in-laravel-queues) [ ![Filament v3 Global Search: Custom Result Providers and Scoped Tenant Isolation](https://cdn.msaied.com/165/4de273593ea2005169698edc5cc53d6b.png) filament laravel multi-tenant 

### Filament v3 Global Search: Custom Result Providers and Scoped Tenant Isolation

Learn how to register custom global search providers in Filament v3, scope results to the current tenant, and...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 14 Jun 2026     1 min read  

  Read    

 ](https://www.msaied.com/articles/filament-v3-global-search-custom-result-providers-and-scoped-tenant-isolation) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
