Testing DDD Components: Isolating Domain Logic in Laravel
Master Testing DDD components in Laravel. Learn to mock external services, isolate domain logic, and write reliable PHPUnit tests for your Action classes.
Previously in this course, we explored distributed tracing to visualize complex execution paths in our SaaS platform. While tracing helps us observe production behavior, we need a proactive strategy to ensure our domain logic remains correct as we scale. This lesson focuses on Testing DDD components, specifically how to isolate business rules from infrastructure, allowing us to evolve our architecture without breaking critical features.
The Philosophy of Domain Isolation
In a Domain-Driven Design (DDD) approach, your core business logic—residing in Action classes and Service layers—should be agnostic of the database, the framework, or external APIs.
If your test suite hits a live Stripe API or requires a complex database state to verify a simple calculation, you aren't testing logic; you're testing the environment. To test effectively, we must strictly separate Unit Tests (logic-focused) from Integration Tests (infrastructure-focused).
Testing Action Classes in Isolation
Action classes are the entry points for your domain logic. To test them without triggering side effects, we rely on dependency injection and testing with test doubles.
Consider a ProcessSubscriptionUpgrade action that depends on a PaymentGatewayInterface.
PHPnamespace Domain\Billing\Actions; use Domain\Billing\Contracts\PaymentGatewayInterface; use Domain\Billing\DTOs\UpgradeData; class ProcessSubscriptionUpgrade { public function __construct( private PaymentGatewayInterface $gateway ) {} public function execute(UpgradeData $data): bool { #6A9955">// Domain logic: validation, calculation if ($data->amount <= 0) return false; #6A9955">// Infrastructure interaction via interface return $this->gateway->charge($data->user, $data->amount); } }
To test this, we don't need a real gateway. We mock the interface.
PHPpublic function test_it_successfully_upgrades_subscription() { $gateway = $this->createMock(PaymentGatewayInterface::class); $gateway->expects($this->once()) ->method('charge') ->willReturn(true); $action = new ProcessSubscriptionUpgrade($gateway); $result = $action->execute(new UpgradeData(user: $user, amount: 100)); $this->assertTrue($result); }
Mocking External Services
When your domain components depend on external services (like an email provider or a CRM), mocking is non-negotiable. Using Laravel's Mockery integration makes this expressive and type-safe.
| Strategy | Best For | Trade-off |
|---|---|---|
| Fakes | Laravel-native services (Mail, Queue, Event) | Less granular control |
| Mocks | Third-party API interfaces | Requires strict contract adherence |
| Stubs | Data-heavy dependencies | Doesn't verify interaction behavior |
If you are calling a third-party service, wrap it in a custom interface. This allows you to use Mockery to verify that your domain layer is passing the correct data structures.
Hands-on Exercise: Refining the Billing Context
In our ongoing SaaS project, navigate to app/Domain/Billing. Identify your primary CreateInvoice action.
- Create a
tests/Unit/Domain/Billing/CreateInvoiceTest.phpfile. - Identify the dependencies: Does it talk to the
TaxCalculatorservice? - Write a test that mocks the
TaxCalculatorto return a fixed value, ensuring yourCreateInvoicelogic correctly adds this tax to the final total. - Assert that the logic holds regardless of the actual tax calculation algorithm.
Common Pitfalls
- Testing Implementation Details: Don't mock private methods or internal Eloquent calls. If your test breaks every time you rename a column, you are testing database schema, not business logic.
- Over-Mocking: If you find yourself mocking 10 different services for a single action, your action class likely violates the Single Responsibility Principle. Break it down into smaller, focused actions.
- Ignoring Integration: Unit tests verify the math, but they don't verify the plumbing. Ensure you have a separate suite of integration tests that verify your Service layer actually connects to your database or message broker correctly.
Recap
Testing DDD components is about creating a clear boundary between "what" your business does and "how" the system executes it. By mocking interfaces and injecting dependencies, you ensure your domain logic is portable, readable, and—most importantly—verifiable without needing a full environment spin-up.
Up next, we will explore Contract Testing to ensure that when our domain services communicate, they stay in sync with their expected interfaces.
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.