Testing Laravel Actions &amp; Boundaries with Pest | 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 Boundary Contracts        On this page       1. [  The Problem with Testing Framework-Coupled Code ](#the-problem-with-testing-framework-coupled-code)
2. [  Actions as the Unit of Work ](#actions-as-the-unit-of-work)
3. [  Testing Actions with Fakes ](#testing-actions-with-fakes)
4. [  Enforcing Boundaries with arch() ](#enforcing-boundaries-with-codearchcode)
5. [  Contract Assertions on Value Objects ](#contract-assertions-on-value-objects)
6. [  Takeaways ](#takeaways)

  ![Clean Architecture Testing with Pest: Actions, Fakes, and Boundary Contracts](https://cdn.msaied.com/257/eb96b08443e07a2edd8694c0f6f8b524.png)

  #laravel   #pest   #clean-architecture   #testing   #ddd  

 Clean Architecture Testing with Pest: Actions, Fakes, and Boundary Contracts 
==============================================================================

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

       Table of contents

1. [  01   The Problem with Testing Framework-Coupled Code  ](#the-problem-with-testing-framework-coupled-code)
2. [  02   Actions as the Unit of Work  ](#actions-as-the-unit-of-work)
3. [  03   Testing Actions with Fakes  ](#testing-actions-with-fakes)
4. [  04   Enforcing Boundaries with arch()  ](#enforcing-boundaries-with-codearchcode)
5. [  05   Contract Assertions on Value Objects  ](#contract-assertions-on-value-objects)
6. [  06   Takeaways  ](#takeaways)

 The Problem with Testing Framework-Coupled Code
-----------------------------------------------

Most Laravel test suites test *routes* and *controllers* end-to-end. That works until your domain logic grows complex enough that a single HTTP test exercises five unrelated concerns. The fix is not more mocking — it is pushing logic into plain PHP actions and testing those in isolation, then asserting that boundaries are respected at the architecture level.

This article shows exactly how to do that with Pest.

---

Actions as the Unit of Work
---------------------------

An action is a single-responsibility class with an `execute` method. It receives a DTO, does one thing, and returns a result. No controller, no request object, no `Auth::user()` calls buried inside.

```php
// app/Domain/Billing/Actions/ChargeSubscription.php
final class ChargeSubscription
{
    public function __construct(
        private readonly PaymentGateway $gateway,
        private readonly SubscriptionRepository $subscriptions,
    ) {}

    public function execute(ChargeSubscriptionData $data): ChargeResult
    {
        $subscription = $this->subscriptions->findOrFail($data->subscriptionId);

        $charge = $this->gateway->charge(
            amount: $data->amount,
            customerId: $subscription->gatewayCustomerId,
        );

        return new ChargeResult(
            chargeId: $charge->id,
            status: $charge->status,
        );
    }
}

```

No `Stripe::charge()` static call. No `request()` helper. Pure dependencies through the constructor — which means Pest can swap them trivially.

---

Testing Actions with Fakes
--------------------------

Create a fake that implements the same interface your action depends on:

```php
// tests/Fakes/FakePaymentGateway.php
final class FakePaymentGateway implements PaymentGateway
{
    public array $charged = [];

    public function charge(Money $amount, string $customerId): ChargeResponse
    {
        $this->charged[] = compact('amount', 'customerId');

        return new ChargeResponse(id: 'ch_fake_123', status: 'succeeded');
    }
}

```

Now the Pest test is fast, deterministic, and readable:

```php
// tests/Unit/Domain/Billing/ChargeSubscriptionTest.php
use App\Domain\Billing\Actions\ChargeSubscription;
use App\Domain\Billing\Data\ChargeSubscriptionData;

beforeEach(function () {
    $this->gateway = new FakePaymentGateway();
    $this->subscriptions = new InMemorySubscriptionRepository();
    $this->action = new ChargeSubscription($this->gateway, $this->subscriptions);
});

it('charges the correct amount to the gateway customer', function () {
    $subscription = $this->subscriptions->create(gatewayCustomerId: 'cus_abc');

    $result = $this->action->execute(new ChargeSubscriptionData(
        subscriptionId: $subscription->id,
        amount: Money::of(4900, 'USD'),
    ));

    expect($result->status)->toBe('succeeded')
        ->and($this->gateway->charged)->toHaveCount(1)
        ->and($this->gateway->charged[0]['customerId'])->toBe('cus_abc');
});

```

No database. No HTTP. Runs in milliseconds.

---

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

Pest's `arch()` helper lets you encode architectural rules as executable tests. This is where clean architecture gets teeth.

```php
// tests/Architecture/DomainTest.php

arch('domain layer has no framework dependencies')
    ->expect('App\Domain')
    ->not->toUse([
        'Illuminate\Http\Request',
        'Illuminate\Support\Facades',
        'Illuminate\Database\Eloquent\Model',
    ]);

arch('actions are final and not extended')
    ->expect('App\Domain\**\Actions')
    ->toBeFinal();

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

```

These run in CI and fail the moment a developer accidentally imports `Auth::user()` inside a domain action. No code review required.

---

Contract Assertions on Value Objects
------------------------------------

Value objects should be immutable and comparable. Test those properties explicitly:

```php
it('is equal to another instance with the same currency and amount', function () {
    $a = Money::of(1000, 'USD');
    $b = Money::of(1000, 'USD');

    expect($a->equals($b))->toBeTrue();
});

it('throws on negative amounts', function () {
    expect(fn () => Money::of(-1, 'USD'))
        ->toThrow(\InvalidArgumentException::class);
});

```

No Laravel bootstrap needed. These are pure PHP tests.

---

Takeaways
---------

- **Actions are the right unit** — they are small enough to test in isolation and large enough to represent real domain work.
- **Fakes beat mocks** for domain boundaries; they are explicit, reusable, and readable.
- **`arch()` rules are living documentation** — they fail loudly when boundaries erode.
- **Readonly DTOs and final actions** are not style preferences; they are constraints that make tests predictable.
- **Keep Eloquent at the edge** — repositories translate between domain objects and persistence, so domain tests never touch the database.

 Found this useful?

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

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

  3 questions  

     Q01  Should I use Mockery or fakes for testing actions?        Prefer hand-written fakes for domain boundaries. Fakes are explicit about what they record and return, making test intent clearer. Mockery is fine for incidental collaborators where writing a full fake would be disproportionate effort. 

      Q02  Does arch() slow down the test suite significantly?        Pest's arch() performs static analysis on your source files rather than executing code, so it adds only a few seconds to a typical suite. Run it in a dedicated CI step if you want to keep your unit test feedback loop fast. 

      Q03  How do I handle Laravel's service container when testing actions in isolation?        You don't need the container for unit tests. Instantiate the action directly with its fakes in beforeEach(). Reserve the container for integration or feature tests where you want to verify the full wiring. 

  Continue reading

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

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

 [ ![Practical RAG in Laravel: pgvector, Embeddings, and Retrieval-Augmented Generation](https://cdn.msaied.com/681/08058424f0e8433b83d9008c6b701cd8.png) laravel ai pgvector 

### Practical RAG in Laravel: pgvector, Embeddings, and Retrieval-Augmented Generation

Build a production-ready RAG pipeline in Laravel using pgvector, OpenAI embeddings, and a clean retrieval laye...

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

 19 Sep 2026     4 min read  

  Read    

 ](https://msaied.com/articles/practical-rag-in-laravel-pgvector-embeddings-and-retrieval-augmented-generation) [ ![Gate Responses, Policy Before-Hooks, and Ownership Guards in Laravel](https://cdn.msaied.com/680/65326929bb7b3e15cee4d9753000eddc.png) laravel authorization security 

### Gate Responses, Policy Before-Hooks, and Ownership Guards in Laravel

Beyond simple true/false gates: learn how to return rich Gate responses, intercept policies with before-hooks,...

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

 19 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/gate-responses-policy-before-hooks-and-ownership-guards-in-laravel) [ ![Fresh: A Laravel Package Skeleton with Testbench, CI, and Boost Integration](https://cdn.msaied.com/679/6e34855eba64d6cc443c5cb2e7d17555.png) Laravel Package Development Orchestra Testbench 

### Fresh: A Laravel Package Skeleton with Testbench, CI, and Boost Integration

Fresh is an opinionated Laravel package skeleton by Mazen Touati that ships with PHPUnit, Larastan, Rector, Pi...

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

 18 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/fresh-a-laravel-package-skeleton-with-testbench-ci-and-boost-integration) 

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