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/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  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) 

 [ ![Laravel AI SDK: Tool-Calling Agents and Conversation Persistence](https://cdn.msaied.com/260/8c84f424e42da01993c9ba4b8eb19655.png) laravel ai agents 

### Laravel AI SDK: Tool-Calling Agents and Conversation Persistence

Build reliable tool-calling AI agents in Laravel using the Prism package. Learn how to wire tools, persist con...

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

 21 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-ai-sdk-tool-calling-agents-and-conversation-persistence) [ ![Laravel Livewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration](https://cdn.msaied.com/259/e8ce445f021c2b26ebe4dd5da50014f8.png) livewire laravel alpine 

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

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

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

 21 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-livewire-v3-internals-morph-markers-js-hooks-and-alpine-integration) [ ![Laravel Package Development: Service Providers, Auto-Discovery, and Config Merging](https://cdn.msaied.com/258/673a80fa8e42ae375a4bba21bdcd92ea.png) laravel packages service-providers 

### Laravel Package Development: Service Providers, Auto-Discovery, and Config Merging

Build a production-ready Laravel package from scratch — covering service provider design, auto-discovery via c...

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

 21 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-package-development-service-providers-auto-discovery-and-config-merging-1) 

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