Testing Laravel Clean Architecture with Pest | 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)    Clean Architecture Testing with Pest: Actions, Fakes, and Architectural Assertions        On this page       1. [  The Problem with Testing "Clean" Code ](#the-problem-with-testing-quotcleanquot-code)
2. [  Testing Actions in Isolation ](#testing-actions-in-isolation)
3. [  Faking Infrastructure at the Service Container Level ](#faking-infrastructure-at-the-service-container-level)
4. [  Enforcing Boundaries with arch() ](#enforcing-boundaries-with-codearchcode)
5. [  Higher-Order Tests for Repetitive Assertions ](#higher-order-tests-for-repetitive-assertions)
6. [  Takeaways ](#takeaways)

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

  #laravel   #pest   #testing   #clean-architecture  

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

     27 Sep 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said  

       Table of contents

1. [  01   The Problem with Testing "Clean" Code  ](#the-problem-with-testing-quotcleanquot-code)
2. [  02   Testing Actions in Isolation  ](#testing-actions-in-isolation)
3. [  03   Faking Infrastructure at the Service Container Level  ](#faking-infrastructure-at-the-service-container-level)
4. [  04   Enforcing Boundaries with arch()  ](#enforcing-boundaries-with-codearchcode)
5. [  05   Higher-Order Tests for Repetitive Assertions  ](#higher-order-tests-for-repetitive-assertions)
6. [  06   Takeaways  ](#takeaways)

 The Problem with Testing "Clean" Code
-------------------------------------

Clean architecture in Laravel — actions, DTOs, value objects, domain services — is only as clean as the tests that guard it. Without deliberate test strategy, you end up with integration tests masquerading as unit tests, and domain logic that silently couples to Eloquent, queues, or HTTP.

Pest gives you the tools to fix this. Here's how to use them properly.

---

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

An action is a single-responsibility class. Test it like one — no HTTP, no database unless the action's explicit contract requires it.

```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
    {
        return $this->users->create([
            'email' => $data->email->value,
            'password' => $this->hasher->make($data->password),
        ]);
    }
}

```

```php
// tests/Unit/Actions/RegisterUserTest.php
use App\Actions\RegisterUser;
use App\Data\RegisterUserData;
use App\Repositories\UserRepository;
use App\ValueObjects\Email;

it('creates a user with a hashed password', function () {
    $repo = Mockery::mock(UserRepository::class);
    $hasher = Mockery::mock(Hasher::class);

    $hasher->expects('make')
        ->with('secret')
        ->andReturn('hashed_secret');

    $repo->expects('create')
        ->with(['email' => 'jane@example.com', 'password' => 'hashed_secret'])
        ->andReturn(new User(['email' => 'jane@example.com']));

    $action = new RegisterUser($repo, $hasher);
    $user = $action->handle(new RegisterUserData(
        email: new Email('jane@example.com'),
        password: 'secret',
    ));

    expect($user->email)->toBe('jane@example.com');
});

```

No `RefreshDatabase`. No HTTP request. Pure unit test.

---

Faking Infrastructure at the Service Container Level
----------------------------------------------------

For feature tests, swap real infrastructure with fakes via `app()->bind()`:

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

it('dispatches a welcome email after registration', function () {
    Mail::fake();

    $this->post('/register', [
        'email' => 'jane@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ])->assertRedirect('/dashboard');

    Mail::assertSent(WelcomeMail::class, fn ($mail) =>
        $mail->hasTo('jane@example.com')
    );
});

```

The `FakeUserRepository` implements the same interface and stores users in memory. Your action never knows the difference.

---

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

Pest's `arch()` helper is the most underused feature in the ecosystem. It lets you write executable architecture rules:

```php
// tests/Arch/DomainTest.php

arch('domain classes do not depend on Illuminate')
    ->expect('App\Domain')
    ->not->toUse('Illuminate\\');

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

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

arch('value objects have no public setters')
    ->expect('App\ValueObjects')
    ->not->toHavePublicMethodsBesides(['value', '__toString', 'equals', 'from']);

```

These run in milliseconds and fail CI the moment someone accidentally injects `Request` into a domain service.

---

Higher-Order Tests for Repetitive Assertions
--------------------------------------------

When you have many value objects with the same contract, higher-order tests eliminate boilerplate:

```php
$validEmails = [
    'simple@example.com',
    'user+tag@sub.domain.org',
];

dataset('valid_emails', $validEmails);

it('accepts valid email addresses', function (string $email) {
    expect(new Email($email))->value->toBe($email);
})->with('valid_emails');

it('rejects malformed email addresses', function (string $email) {
    expect(fn () => new Email($email))->toThrow(InvalidEmailException::class);
})->with([
    'not-an-email',
    '@nodomain',
    'missing@',
]);

```

---

Takeaways
---------

- **Inject interfaces, not concretions** — makes unit testing actions trivial without a database.
- **Use `arch()` assertions** to enforce that domain code stays framework-agnostic; run them in CI.
- **Bind fakes in `beforeEach`** for feature tests rather than hitting real infrastructure.
- **`readonly` DTOs + `final` actions** are enforceable with Pest arch rules, not just convention.
- **Datasets** keep value object tests exhaustive without copy-paste test methods.
- Keep unit tests in `tests/Unit` and feature tests in `tests/Feature`; never let a unit test touch the database accidentally.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fclean-architecture-testing-with-pest-actions-fakes-and-architectural-assertions-1&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%2Fwww.msaied.com%2Farticles%2Fclean-architecture-testing-with-pest-actions-fakes-and-architectural-assertions-1) 

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

  3 questions  

     Q01  Does Pest's arch() work with namespaces that use PSR-4 subdirectories?        Yes. Pest resolves classes via Composer's autoloader, so any PSR-4 namespace like `App\Domain` maps correctly to `app/Domain`. Just ensure your composer.json autoload section covers the namespace. 

      Q02  Should I use Mockery or Pest's built-in mock() helper for action tests?        Pest's `mock()` helper wraps Mockery under the hood and integrates with expectations cleanly. For simple interface fakes, a hand-written fake class is often more readable and avoids mock assertion order surprises. 

      Q03  How do I prevent arch() tests from slowing down the suite?        Arch tests are static analysis over loaded class files — they don't boot the application or hit the database. Group them in a dedicated `tests/Arch` directory and run them as a separate Pest group in CI if needed, though in practice they're fast enough to run with every suite. 

  Continue reading

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

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

 [ ![Multi-Tenant SaaS with Laravel: Scoping Queries, Resolving Tenants, and Isolating State](https://cdn.msaied.com/708/08ce1de79b1d408fbd91c91ddcb3f056.png) laravel multi-tenancy saas 

### Multi-Tenant SaaS with Laravel: Scoping Queries, Resolving Tenants, and Isolating State

A practical deep-dive into building multi-tenant SaaS with Laravel — covering tenant resolution middleware, au...

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

 27 Sep 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/multi-tenant-saas-with-laravel-scoping-queries-resolving-tenants-and-isolating-state) [ ![Livewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration](https://cdn.msaied.com/707/0eb21e520c1216424fd97efe3608f4db.png) livewire laravel alpine 

### Livewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration

Go beyond the docs: understand how Livewire v3 diffs and patches the DOM with morph markers, intercept the lif...

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

 27 Sep 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/livewire-v3-internals-morph-markers-js-hooks-and-alpine-integration-5) [ ![Typed PHP 8.3 Enums as Eloquent Casts, Route Parameters, and Validation Rules](https://cdn.msaied.com/706/a9d051bc039469c10d1d4c5fc364e598.png) laravel php8.3 enums 

### Typed PHP 8.3 Enums as Eloquent Casts, Route Parameters, and Validation Rules

Go beyond basic enum casting. Learn how to wire PHP 8.3 enums into Eloquent, bind them as route model paramete...

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

 26 Sep 2026     1 min read  

  Read    

 ](https://www.msaied.com/articles/typed-php-83-enums-as-eloquent-casts-route-parameters-and-validation-rules-1) 

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