Unit Testing Clean Architecture in Java
Fast recall
Quick revision
Core answer
@WebMvcTest slice with mocked use cases. Persistence adapters — Testcontainers + real Postgres (not H2). External adapters — WireMock for HTTP gateways. Use @SpringBootTest sparingly for end-to-end smoke tests of critical flows. The result: most logic tested in milliseconds without a Spring context.Different test strategies per Clean Architecture layer, why in-memory implementations beat heavy Mockito stubs for use case tests, and minimizing reliance on full Spring context.
Using `@SpringBootTest` for everything (slow), or mocking every method on every dependency (brittle, low-signal tests).
Discuss test pyramid in Clean Architecture, fakes vs mocks, Testcontainers over H2 for persistence tests, and contract tests for external adapters.
Interview-ready guide
Interview answer
Why Clean Architecture helps testing
Dependency rule pays the biggest dividend at test time. Entities and use cases have zero framework dependencies — plain JUnit in milliseconds. Adapters are the only place where infrastructure runs — test in slices without full app.
Tests pyramid naturally: thousands of fast unit tests for business logic, hundreds of slice tests for adapters, dozens of integration tests for end-to-end.
Layer 1 — Entities + value objects
Plain JUnit + AssertJ. No Spring, no Mockito, no DB. Test invariants in constructors, behavior of business methods, value object equality. Microseconds per test, bulk of the suite, covers logic-dense code.
Layer 2 — Use cases (interactors)
Plain JUnit + in-memory implementations of ports. Why fakes over mocks: fakes behave like real adapters, work across multiple calls, tests are behavior-focused not implementation-coupled. Mocking 8 methods on 3 dependencies is brittle.
InMemoryOrderRepository, InMemoryInventoryGateway implementing the ports. PlaceOrderInteractorTest constructs them directly, calls use case, asserts side effects. No Spring, no @SpringBootTest. 100 tests in under a second.
Layer 3 — Web adapter (@WebMvcTest)
Spring slice loading only the web layer — controllers, exception handlers, validators. Not full app context.
Test request mapping, validation, JSON serialization, status codes, error handling. Not business logic — use case tests cover that.
@WebMvcTest(OrderController.class), @MockBean for the use case, MockMvc for the request. Fast (1-2s for slice).
Layer 4 — Persistence adapter (Testcontainers)
Testcontainers + real Postgres. Avoid H2 — different SQL dialect, tests pass that fail in production.
Test mapper between domain and JPA entity, repository queries, native queries, transaction behavior.
@DataJpaTest + @Testcontainers + PostgreSQLContainer. Real Postgres catches dialect issues.
Layer 5 — External adapters
WireMock for HTTP gateways, embedded brokers for Kafka. Stub Stripe response, verify gateway parses it. Test card-declined error path.
For critical externals, contract tests (Pact, Spring Cloud Contract) catch schema changes.
When to use @SpringBootTest
Sparingly — for critical end-to-end smoke tests. Goals: verify wiring works (all beans resolve, profiles load, security config OK), verify happy-path goes HTTP → use case → DB → response.
5-10 tests, not 500. Slow tests = ignored tests.
Common mistakes
- @SpringBootTest for everything — pyramid inverts, CI takes 30 minutes
- Mocking 10 methods on 3 dependencies — brittle, breaks on every refactor
- H2 instead of Testcontainers — production-only bugs
- Testing through controllers what use case test should cover — slow, redundant, fragile
- No fakes for ports — re-mocking same dependency every test
- No tests for exception paths — happy-path only misses important behavior
- Coverage as a goal — 100% on getters/setters tells you nothing
Recommendation
Test by layer with the right tool. Plain JUnit + in-memory port implementations for entities and use cases (microseconds, the bulk of the suite). Spring slices (@WebMvcTest, @DataJpaTest with Testcontainers) for adapters. WireMock + contract tests for external dependencies. Reserve @SpringBootTest for 5-10 critical end-to-end smoke tests. Prefer fakes over mocks for use cases — less brittle, more behavior-focused. Avoid H2 in favor of Testcontainers + the real DB engine. Speed matters: a fast test suite gets run on every save; a slow one gets skipped, and the value disappears.
Unit Testing Clean Architecture in Java
Direct Answer
In Clean Architecture, unit tests target each layer with the right tool: Entities and value objects — plain JUnit, no framework. Use cases (interactors) — plain JUnit + in-memory port implementations (preferred over Mockito mocks). Web adapters — Spring's @WebMvcTest slice with mocked use cases. Persistence adapters — Testcontainers + real Postgres (not H2). External adapters — WireMock for HTTP gateways. Use @SpringBootTest sparingly for end-to-end smoke tests of critical flows. The result: most logic tested in milliseconds without a Spring context.
Common Mistakes & Pitfalls
Using @SpringBootTest for everything (slow), or mocking every method on every dependency (brittle, low-signal tests).
Check your understanding
Answer these without looking back at the article.
- Why is in-memory port preferred over Mockito for use case tests?
- Walk through a Testcontainers test for a JPA repository.
- When would you use `@SpringBootTest` over `@WebMvcTest`?
- How do you test a Kafka consumer adapter?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.