Pest Testing: Actions, Fakes &amp; Arch Assertions | 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)    Clean Architecture Testing with Pest: Actions, Fakes, and Architectural Assertions        On this page       1. [  Why Clean Architecture and Pest Are a Natural Pair ](#why-clean-architecture-and-pest-are-a-natural-pair)
2. [  Testing Actions in Isolation ](#testing-actions-in-isolation)
3. [  Fake Collaborators via the Service Container ](#fake-collaborators-via-the-service-container)
4. [  Enforcing Boundaries with arch() ](#enforcing-boundaries-with-codearchcode)
5. [  Combining Expectations for Richer Assertions ](#combining-expectations-for-richer-assertions)
6. [  Takeaways ](#takeaways)

  ![Clean Architecture Testing with Pest: Actions, Fakes, and Architectural Assertions](https://cdn.msaied.com/388/fc5d6303a4c8676ae736106bdc4bfcce.png)

  #laravel   #pest   #testing   #clean-architecture  

 Clean Architecture Testing with Pest: Actions, Fakes, and Architectural Assertions 
====================================================================================

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

       Table of contents

1. [  01   Why Clean Architecture and Pest Are a Natural Pair  ](#why-clean-architecture-and-pest-are-a-natural-pair)
2. [  02   Testing Actions in Isolation  ](#testing-actions-in-isolation)
3. [  03   Fake Collaborators via the Service Container  ](#fake-collaborators-via-the-service-container)
4. [  04   Enforcing Boundaries with arch()  ](#enforcing-boundaries-with-codearchcode)
5. [  05   Combining Expectations for Richer Assertions  ](#combining-expectations-for-richer-assertions)
6. [  06   Takeaways  ](#takeaways)

 Why Clean Architecture and Pest Are a Natural Pair
--------------------------------------------------

Clean architecture splits your Laravel app into layers — domain actions, DTOs, repositories — but without test discipline those layers collapse back into fat controllers within months. Pest's fluent API, `arch()` plugin, and first-class fake support give you three distinct levers to keep boundaries honest: **unit-level action tests**, **fake collaborators that replace the service container**, and **static architectural assertions** that run in CI.

---

Testing Actions in Isolation
----------------------------

An action is a single-responsibility class. Test it without booting the framework.

```php
// app/Actions/RegisterUser.php
final class RegisterUser
{
    public function __construct(
        private readonly UserRepository $users,
        private readonly Hasher $hasher,
    ) {}

    public function handle(RegisterUserData $data): User
    {
        $hashed = $this->hasher->make($data->password);
        return $this->users->create($data->email, $hashed);
    }
}

```

```php
// tests/Unit/Actions/RegisterUserTest.php
use App\Actions\RegisterUser;
use App\Data\RegisterUserData;

it('hashes the password before persisting', function () {
    $hasher = Mockery::mock(Hasher::class);
    $hasher->expects('make')->with('secret')->andReturn('hashed');

    $repo = Mockery::mock(UserRepository::class);
    $repo->expects('create')
         ->with('alice@example.com', 'hashed')
         ->andReturn(new User);

    $action = new RegisterUser($repo, $hasher);
    $action->handle(new RegisterUserData('alice@example.com', 'secret'));
});

```

No `RefreshDatabase`, no HTTP overhead. These tests run in milliseconds.

---

Fake Collaborators via the Service Container
--------------------------------------------

For integration-level tests you still want real Laravel wiring but fake I/O. Bind a fake inside a `beforeEach` rather than scattering `Mail::fake()` calls everywhere.

```php
// tests/Feature/RegisterUserFeatureTest.php
use App\Contracts\UserRepository;
use App\Fakes\InMemoryUserRepository;

beforeEach(function () {
    $this->app->bind(UserRepository::class, InMemoryUserRepository::class);
});

it('stores a new user through the HTTP layer', function () {
    $response = $this->postJson('/register', [
        'email'    => 'bob@example.com',
        'password' => 'password',
    ]);

    $response->assertCreated();

    $repo = $this->app->make(UserRepository::class);
    expect($repo->all())->toHaveCount(1)
        ->and($repo->all()->first()->email)->toBe('bob@example.com');
});

```

`InMemoryUserRepository` implements the same contract as the Eloquent version — no database, no migrations, deterministic.

---

Enforcing Boundaries with `arch()`
----------------------------------

Pest's `arch()` helper turns architectural rules into executable tests. Add these to a dedicated `tests/ArchTest.php`.

```php
arch('domain layer has no framework dependencies')
    ->expect('App\Domain')
    ->toUseNothing()
    ->ignoring('Illuminate\Contracts'); // interfaces are fine

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

arch('DTOs are readonly')
    ->expect('App\Data')
    ->toBeReadonly();

arch('repositories live behind contracts')
    ->expect('App\Repositories')
    ->toImplement('App\Contracts\Repository');

```

These assertions run statically — no database, no HTTP. They fail the moment a junior dev imports `Illuminate\Http\Request` inside a domain class.

---

Combining Expectations for Richer Assertions
--------------------------------------------

Pest's expectation API chains cleanly with custom higher-order helpers.

```php
expect(RegisterUserData::class)
    ->toBeReadonly()
    ->and(RegisterUserData::class)
    ->toHaveConstructor()
    ->and(new RegisterUserData('a@b.com', 'pw'))
    ->email->toBe('a@b.com');

```

This single chain verifies the DTO is immutable, constructable, and maps properties correctly — three concerns, one readable block.

---

Takeaways
---------

- **Test actions directly** with constructor injection; skip the framework bootstrap for pure domain logic.
- **Bind in-memory fakes** in `beforeEach` rather than using static facade fakes — your tests stay container-aware without coupling to concrete implementations.
- **`arch()` assertions are free CI guards** — they catch boundary violations before code review.
- **Readonly DTOs + final actions** are enforceable conventions, not just style guidelines, when you back them with Pest.
- Keep a single `ArchTest.php` as the living documentation of your architectural rules.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fclean-architecture-testing-with-pest-actions-fakes-and-architectural-assertions&text=Clean+Architecture+Testing+with+Pest%3A+Actions%2C+Fakes%2C+and+Architectural+Assertions) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fclean-architecture-testing-with-pest-actions-fakes-and-architectural-assertions) 

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

  3 questions  

     Q01  Does arch() perform runtime analysis or static analysis?        Pest's arch() uses static reflection — it inspects class metadata without executing code. This makes it fast and safe to run in CI without a database or running application. 

      Q02  When should I use an InMemoryRepository fake versus RefreshDatabase?        Use an in-memory fake when testing application logic that flows through a repository contract. Use RefreshDatabase when you specifically need to verify SQL queries, migrations, or Eloquent behaviour itself. 

      Q03  Can arch() assertions check for prohibited imports across namespaces?        Yes. Use -&gt;toUseNothing() or -&gt;toUseStrictly(['AllowedNamespace']) to whitelist exactly which dependencies a namespace may import, catching accidental cross-layer coupling automatically. 

  Continue reading

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

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

 [ ![Building a Laravel Package: Service Providers, Auto-Discovery, and Config Merging](https://cdn.msaied.com/581/f2ebb3b6b30fffad55642b4f8e8d6ee1.png) laravel packages service-providers 

### Building a Laravel Package: Service Providers, Auto-Discovery, and Config Merging

A practical deep-dive into authoring a production-ready Laravel package — covering service provider design, au...

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

 22 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/building-a-laravel-package-service-providers-auto-discovery-and-config-merging-3) [ ![Laravel Reverb in Production: Scaling WebSockets Beyond a Single Server](https://cdn.msaied.com/580/851fec3976838708af1706f705fe70cd.png) laravel reverb websockets 

### Laravel Reverb in Production: Scaling WebSockets Beyond a Single Server

Running Laravel Reverb on a single node is easy. Scaling it across multiple workers, handling reconnects grace...

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

 22 Aug 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-reverb-in-production-scaling-websockets-beyond-a-single-server-1) [ ![Filament v3 to v4 Migration: Breaking Changes and Practical Refactor Patterns](https://cdn.msaied.com/579/88c4b61835f17b2248e3e39a0e3e765f.png) filament laravel upgrade 

### Filament v3 to v4 Migration: Breaking Changes and Practical Refactor Patterns

Upgrading from Filament v3 to v4 touches forms, tables, actions, and the panel provider API. This guide walks...

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

 22 Aug 2026     4 min read  

  Read    

 ](https://msaied.com/articles/filament-v3-to-v4-migration-breaking-changes-and-practical-refactor-patterns-2) 

   [  ![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)
