Pest Architecture Tests for Clean Laravel Apps | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Pest Architecture Tests: Enforcing Clean Boundaries in Laravel Without a CI Nanny

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

 Pest's architecture testing API lets you encode structural rules directly in your test suite, catching layer violations before they reach review. Here's how to use it effectively in a real Laravel project.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 14 Jun 2026 · Updated 14 Jun 2026 · 3 min read

ShareCopy linkCopied

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

  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)

 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.

- [pest](https://www.msaied.com/public/articles?search=pest)
- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [testing](https://www.msaied.com/public/articles?search=testing)
- [clean-architecture](https://www.msaied.com/public/articles?search=clean-architecture)
- [php](https://www.msaied.com/public/articles?search=php)

 Frequently asked questions 
---------------------------

  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.

   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.

   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.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

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

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleFilament v3 Global Search: Custom Result Providers and Scoped Tenant Isolation](https://www.msaied.com/public/articles/filament-v3-global-search-custom-result-providers-and-scoped-tenant-isolation) [Next articleJob Batching, Chaining, and Rate-Limited Middleware in Laravel Queues](https://www.msaied.com/public/articles/job-batching-chaining-and-rate-limited-middleware-in-laravel-queues)  

   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)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
