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

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

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

  [ home ](https://msaied.com)    [ articles ](https://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%2Fmsaied.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%2Fmsaied.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://msaied.com/articles) 

 [ ![Practical RAG in Laravel: pgvector, Embeddings, and Retrieval Pipelines](https://cdn.msaied.com/487/2db17c4f7927d4a229a4786c03e7b67b.png) laravel ai pgvector 

### Practical RAG in Laravel: pgvector, Embeddings, and Retrieval Pipelines

Build a production-ready retrieval-augmented generation pipeline in Laravel using pgvector, OpenAI embeddings,...

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

 30 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/practical-rag-in-laravel-pgvector-embeddings-and-retrieval-pipelines-2) [ ![Laravel Octane + FrankenPHP: Persistent Services, Boot-Once Singletons, and Safe State](https://cdn.msaied.com/486/7a50572cb8b282ae36063a9213f55ed3.png) laravel octane frankenphp 

### Laravel Octane + FrankenPHP: Persistent Services, Boot-Once Singletons, and Safe State

Running Laravel under FrankenPHP with Octane means your service container lives across requests. Learn which s...

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

 30 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-octane-frankenphp-persistent-services-boot-once-singletons-and-safe-state) [ ![Everything Announced at Laracon US 2026: Laravel Framework, Cloud & AI Updates](https://cdn.msaied.com/485/8c49e044ff45a776167065579e1c93ac.png) Laracon 2026 Laravel Cloud Laravel AI SDK 

### Everything Announced at Laracon US 2026: Laravel Framework, Cloud &amp; AI Updates

Laracon US 2026 brought major updates across the Laravel ecosystem: scale-to-zero Flex compute, managed queues...

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

 29 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/everything-announced-at-laracon-us-2026-laravel-framework-cloud-ai-updates) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://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://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://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)
