Pest Testing: Actions, Fakes &amp; Arch Rules in Laravel | 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 Rules        On this page       1. [  The Problem With Testing Laravel Actions ](#the-problem-with-testing-laravel-actions)
2. [  A Concrete Action Worth Testing ](#a-concrete-action-worth-testing)
3. [  Testing Strategy: Real vs Fake ](#testing-strategy-real-vs-fake)
4. [  Writing the In-Memory Fake ](#writing-the-in-memory-fake)
5. [  Pest Specs for the Action ](#pest-specs-for-the-action)
6. [  Enforcing Architectural Rules with arch() ](#enforcing-architectural-rules-with-codearchcode)
7. [  Takeaways ](#takeaways)

  ![Clean Architecture Testing with Pest: Actions, Fakes, and Architectural Rules](https://cdn.msaied.com/467/8af6bf2c4d59fde5a6e215794acde41b.png)

  #laravel   #pest   #testing   #clean-architecture  

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

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

       Table of contents

1. [  01   The Problem With Testing Laravel Actions  ](#the-problem-with-testing-laravel-actions)
2. [  02   A Concrete Action Worth Testing  ](#a-concrete-action-worth-testing)
3. [  03   Testing Strategy: Real vs Fake  ](#testing-strategy-real-vs-fake)
4. [  04   Writing the In-Memory Fake  ](#writing-the-in-memory-fake)
5. [  05   Pest Specs for the Action  ](#pest-specs-for-the-action)
6. [  06   Enforcing Architectural Rules with arch()  ](#enforcing-architectural-rules-with-codearchcode)
7. [  07   Takeaways  ](#takeaways)

 The Problem With Testing Laravel Actions
----------------------------------------

Most Laravel test suites drift toward one of two failure modes: they either test too much (full HTTP round-trips for every edge case) or too little (mocking so aggressively that the test proves nothing). Actions — single-responsibility classes that encapsulate one piece of business logic — sit in the sweet spot, but only if you test them correctly.

This article shows a concrete, opinionated approach: test actions in isolation with real collaborators where cheap, use targeted fakes where expensive, and enforce boundaries with Pest's `arch()` API.

---

A Concrete Action Worth Testing
-------------------------------

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

    public function handle(RegisterUserData $data): User
    {
        if ($this->users->existsByEmail($data->email)) {
            throw new EmailAlreadyTaken($data->email);
        }

        $user = $this->users->create([
            'name'     => $data->name,
            'email'    => $data->email,
            'password' => $this->hasher->make($data->password),
        ]);

        $this->events->dispatch(new UserRegistered($user));

        return $user;
    }
}

```

Three collaborators: a repository, the event dispatcher, and the hasher. Each has a different testing cost.

---

Testing Strategy: Real vs Fake
------------------------------

| Collaborator | Strategy | Reason | |---|---|---| | `Hasher` | Real (`bcrypt`) | Pure, fast, no I/O | | `UserRepository` | In-memory fake | Avoids DB migrations in unit tests | | `Dispatcher` | Laravel's `Event::fake()` | Lets us assert events dispatched |

### Writing the In-Memory Fake

```php
// tests/Fakes/InMemoryUserRepository.php
final class InMemoryUserRepository implements UserRepository
{
    /** @var array */
    private array $store = [];
    private int $nextId = 1;

    public function existsByEmail(string $email): bool
    {
        return collect($this->store)
            ->contains(fn(User $u) => $u->email === $email);
    }

    public function create(array $attributes): User
    {
        $user = new User(array_merge($attributes, ['id' => $this->nextId++]));
        $this->store[] = $user;
        return $user;
    }
}

```

No database, no migrations, no `RefreshDatabase`. The test suite stays fast.

---

Pest Specs for the Action
-------------------------

```php
// tests/Unit/Actions/RegisterUserTest.php
use App\Actions\RegisterUser;
use App\Events\UserRegistered;
use App\Data\RegisterUserData;
use App\Exceptions\EmailAlreadyTaken;
use Tests\Fakes\InMemoryUserRepository;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Hash;

beforeEach(function () {
    Event::fake();
    $this->repo   = new InMemoryUserRepository();
    $this->action = new RegisterUser($this->repo, app(Dispatcher::class), Hash::getFacadeRoot());
});

it('creates a user and dispatches UserRegistered', function () {
    $data = new RegisterUserData('Alice', 'alice@example.com', 'secret123');

    $user = $this->action->handle($data);

    expect($user->email)->toBe('alice@example.com');
    expect(Hash::check('secret123', $user->password))->toBeTrue();
    Event::assertDispatched(UserRegistered::class,
        fn($e) => $e->user->email === 'alice@example.com'
    );
});

it('throws EmailAlreadyTaken when email exists', function () {
    $data = new RegisterUserData('Alice', 'alice@example.com', 'secret123');
    $this->action->handle($data); // first registration

    expect(fn() => $this->action->handle($data))
        ->toThrow(EmailAlreadyTaken::class);
});

```

No HTTP, no database, no `$this->artisan`. Each test runs in microseconds.

---

Enforcing Architectural Rules with `arch()`
-------------------------------------------

Pest's `arch()` API lets you encode boundaries as failing tests — not just documentation.

```php
// tests/Arch/DomainTest.php
arch('actions are final and have no public properties')
    ->expect('App\Actions')
    ->toBeFinal()
    ->and('App\Actions')
    ->not->toHavePublicProperties();

arch('domain layer does not depend on Illuminate HTTP')
    ->expect('App\Domain')
    ->not->toUse('Illuminate\Http');

arch('value objects are readonly')
    ->expect('App\Values')
    ->toBeReadonly();

arch('actions only depend on contracts, not Eloquent models directly')
    ->expect('App\Actions')
    ->not->toUse('Illuminate\Database\Eloquent\Model');

```

These rules run on every CI push. A junior developer who reaches for `User::find()` inside an action gets a red build, not a code review comment three days later.

---

Takeaways
---------

- **Fake at the boundary, not everywhere.** Use real implementations for pure collaborators; reserve fakes for I/O.
- **In-memory fakes are faster and more honest than Mockery mocks** — they exercise the contract, not just the call signature.
- **`arch()` rules are executable architecture docs.** Write them early; they pay dividends as the team grows.
- **Actions should be `final`.** It prevents accidental inheritance and signals single-responsibility intent.
- **Avoid `RefreshDatabase` in unit tests.** Reserve it for integration tests that genuinely need persistence.

 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-rules&text=Clean+Architecture+Testing+with+Pest%3A+Actions%2C+Fakes%2C+and+Architectural+Rules) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fclean-architecture-testing-with-pest-actions-fakes-and-architectural-rules) 

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

  3 questions  

     Q01  When should I use Event::fake() versus a real event dispatcher in action tests?        Use Event::fake() whenever the test's purpose is to verify that an event was dispatched with the correct payload. If you need to test a listener's side effects, write a separate integration test that lets the event propagate through a real dispatcher. 

      Q02  Do Pest arch() rules slow down the test suite significantly?        No. Pest's architectural assertions perform static analysis on the class map rather than executing code, so they add only a few seconds to a full suite run and can be grouped into a dedicated arch test file that runs separately from unit tests if needed. 

      Q03  Should every action be tested in isolation, or are feature tests sufficient?        Both have a role. Isolated unit tests on actions give fast, precise feedback on business logic. Feature tests verify that the HTTP layer, middleware, and action wire together correctly. Relying solely on feature tests makes failures harder to diagnose and the suite slower. 

  Continue reading

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

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

 [ ![Filament v4 Migrating from v3: Breaking Changes and Refactor Patterns](https://cdn.msaied.com/472/bb7fbf8441c775fcfadd23850e1756e8.png) filament laravel migration 

### Filament v4 Migrating from v3: Breaking Changes and Refactor Patterns

A practical, opinionated guide to the most impactful Filament v3→v4 breaking changes: schema-based forms, the...

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

 26 Jul 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/filament-v4-migrating-from-v3-breaking-changes-and-refactor-patterns) [ ![Concurrency in Laravel: Process Pools, Fibers, and the Concurrency Facade](https://cdn.msaied.com/471/ba1a628c196e92cbb81033c865c9ad26.png) laravel concurrency php 

### Concurrency in Laravel: Process Pools, Fibers, and the Concurrency Facade

Laravel's Concurrency facade and process pools let you run independent work in parallel without reaching for a...

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

 26 Jul 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/concurrency-in-laravel-process-pools-fibers-and-the-concurrency-facade) [ ![Laravel Queues in Production: Horizon Tuning, Metrics, and Graceful Scaling](https://cdn.msaied.com/470/afd2bee56d15842d72c1c6d5c238d236.png) laravel horizon queues 

### Laravel Queues in Production: Horizon Tuning, Metrics, and Graceful Scaling

Go beyond basic queue setup. Learn how to tune Horizon supervisor processes, interpret queue metrics, handle b...

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

 26 Jul 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-queues-in-production-horizon-tuning-metrics-and-graceful-scaling) 

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