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) 

 [ ![HTTP Query Method Support in Laravel 13.19](https://cdn.msaied.com/394/4c917aad559beddb5eeb9a7a1e107f4c.png) Laravel Laravel 13 HTTP Client 

### HTTP Query Method Support in Laravel 13.19

Laravel 13.19 adds Http::query() for the HTTP QUERY verb, query/queryJson testing helpers, a reduceInto() coll...

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

 8 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/http-query-method-support-in-laravel-1319) [ ![Blackfire & Xdebug Profiling in Laravel: Finding Real Bottlenecks in Production-Like Environments](https://cdn.msaied.com/393/a8dfc4ec52c4febc3ef41a491eb452ea.png) laravel performance profiling 

### Blackfire &amp; Xdebug Profiling in Laravel: Finding Real Bottlenecks in Production-Like Environments

Stop guessing where your Laravel app is slow. This guide walks through practical Blackfire and Xdebug profilin...

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

 8 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/blackfire-xdebug-profiling-in-laravel-finding-real-bottlenecks-in-production-like-environments) [ ![Laravel Cloud Security Defaults Behind Every Deploy](https://cdn.msaied.com/395/28df8f542fbe81ecee3ba4ad897f36d6.png) Laravel Cloud Security PHP 

### Laravel Cloud Security Defaults Behind Every Deploy

Laravel Cloud ships WAF rules, DDoS mitigation, automatic PHP patching, SOC 2 Type II compliance, and deploy-t...

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

 8 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-cloud-security-defaults-behind-every-deploy) 

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