Pest Testing: Actions, Fakes &amp; Arch Rules in Laravel | 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. Clean Architecture Testing with Pest: Actions, Fakes, and Architectural Rules

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

 Learn how to test Laravel actions, enforce architectural boundaries, and write expressive Pest specs that catch real bugs — without mocking everything into meaninglessness.

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

ShareCopy linkCopied

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

  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)

 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.

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

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

  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.

   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.

   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.

   ![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 articleProduction AI Agents in Laravel: Streaming, Token Budgets, and Structured Output Contracts](https://www.msaied.com/public/articles/production-ai-agents-in-laravel-streaming-token-budgets-and-structured-output-contracts-3) [Next articleMulti-Tenant SaaS with Laravel + Filament: Scoping Queries, Panels, and Jobs per Tenant](https://www.msaied.com/public/articles/multi-tenant-saas-with-laravel-filament-scoping-queries-panels-and-jobs-per-tenant)  

   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)

 ###  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)
