Testing Strategy for Domain-Driven Design: Unit, Integration & Contract Tests
A Clean Architecture codebase enables a testing pyramid where the domain layer (pure business logic) is covered by fast, in-memory unit tests; the application layer is tested with repository and event bus doubles; and only the infrastructure layer needs slow integration tests with real databases. This article defines the exact test strategy for the billing domain.
Backend Clean Architecture & Domain-Driven Design
Testing Strategy for Domain-Driven Design: Unit, Integration & Contract Tests
Domain objects are autonomous state machines with enforced invariants — they are not passive bags of data passed between controller functions. This architectural property has a direct consequence for testing: because the Domain layer has zero infrastructure dependencies, it can be tested in pure in-memory unit tests that run in milliseconds. Because the Infrastructure layer implements well-defined interfaces, it can be tested against contract suites that validate behavioral equivalence. Because the Application layer depends only on ports, it can be tested with in-memory doubles for all external dependencies.
This article builds the complete testing strategy for the billing engine across all four layers — defining the test pyramid, implementing every test category, and assembling the suite that validates 237 behaviors in under 6 seconds.
Prerequisites: All previous parts, particularly Part 4 (Repository Contract Tests), Part 5 (SpyEventBus), and Part 9 (Test Container). This article consolidates, extends, and codifies the testing patterns introduced individually throughout the series into a single, authoritative reference.
1. The Test Pyramid for Clean Architecture
| Tier | Count | Time | Dependencies | Runs In |
|---|---|---|---|---|
| Domain Unit | 142 | 0.8s | None | vitest run on every save |
| Application Unit | 67 | 1.2s | In-memory doubles | CI pre-merge |
| Infrastructure Contract | 28 | 4.1s | Docker Compose (Postgres) | CI pre-merge |
| E2E / HTTP | 12 | 2.3s | Supertest + test container | CI pre-merge |
| Total | 249 | ~8.4s |
The 80% principle: 80% of behaviors are covered by domain and application tests that require zero infrastructure. Infrastructure contract tests cover the remaining 20% — the persistence, serialization, and adapter translation behaviors that cannot be tested without a real database.
2. Domain Unit Tests: Pure Business Logic
Domain unit tests are the fastest and most valuable tests in the suite. They test the business rules embedded in Aggregates, Value Objects, and Specifications — with zero setup, zero mocking, and zero infrastructure.
2.1 Testing Value Objects
2.2 Testing Aggregate Invariants
2.3 Testing Specifications
Parameterized tests with it.each — each scenario is a single data row. Adding a new boundary case is one line.
3. Application Layer Tests: Use Cases with In-Memory Doubles
Application tests validate orchestration behavior — that the use case calls the right domain methods, saves the Aggregate, and dispatches the correct events. They use real domain objects but in-memory infrastructure doubles.
4. Infrastructure Contract Tests: Behavioral Equivalence
Contract tests run both InMemoryOrderRepository and PrismaOrderRepository against the same suite, ensuring behavioral equivalence. From Part 4:
Both suites run the same 12 assertions. If PrismaOrderRepository passes all 12, it is behaviorally equivalent to InMemoryOrderRepository and can be used interchangeably in production.
5. E2E HTTP Tests: Full Stack with Supertest
E2E tests drive the HTTP interface with supertest, using the test container from Part 9 (all in-memory doubles):
These tests exercise the full stack: HTTP parsing → validation middleware → controller → use case → domain → in-memory repository → event bus spy. No database. No Stripe. Full path in under 20ms per test.
6. Test Organization and CI Pipeline
6.1 Directory Structure
6.2 Vitest Configuration
6.3 Test Data Builders: The Builder Pattern for Tests
Avoid new Order(...) calls scattered across tests — they break when the constructor signature changes:
When Order.create() gains a new required parameter, fix it in OrderBuilder — zero test files need updating.
Summary
| Test Category | Scope | Speed | Infrastructure | Primary Assertion |
|---|---|---|---|---|
| Domain Unit | Aggregates, Value Objects, Specifications | < 1ms each | None | Business rule enforcement |
| Application Unit | Use Cases via in-memory doubles | < 5ms each | None | Orchestration correctness, event dispatch |
| Infrastructure Contract | Both InMemory and Prisma against shared suite | ~100ms each | Postgres (Docker) | Behavioral equivalence across implementations |
| E2E HTTP | Full stack with supertest + test container | < 20ms each | None | HTTP → domain → response path |
What's Next
In Part 13, we implement
PrismaOrderRepositoryin full detail — the Anti-Corruption Layer that translates between flat Prisma rows and rich domain Aggregates, handling thetoDomain()mapping, optimistic concurrency viaversion, and batch-safecreateManywithin a single$transaction. Part 13: Persistence & Repository →
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.