Verdict
No
No, do not give every layer its own tests; test each rule once in the layer that holds it, and let one endpoint test per main path run through all layers against a real database.
Why
- Blast radiususers
- Change frequencyregularly
- Detectabilityimmediately
- Reversibilitytrivial
- Test costmoderate
Do not write separate tests for every layer. The typical case is a web backend with controller, service and repository layers, whose endpoint tests run each route against a real database in CI with a distinct value in each field. Blast radius is users and Change frequency is regularly, because most features touch all three layers. Detectability is immediately, because a broken layer fails the endpoint test before merge, so Reversibility is trivial. Test cost is moderate, because each layer test needs a mock of the layer below, and rule R8 gives Do not test; a layer whose rules or queries the endpoint tests do not tell apart gives Test.
| When | Decision | Why |
|---|---|---|
| The service layer holds branching rules, such as working-day due dates, and the endpoint tests run one branch | Test: unit tests in the service layer for each branch and boundary | Detectability moves to eventually and Reversibility to with-effort, because a wrong saved date looks plausible |
| The repository layer has a hand-written query with filters, and the endpoint tests use rows that every filter lets through | Test: an integration test against a real database with rows that each filter must leave out | Detectability moves to eventually and Reversibility to with-effort, because the query returns plausible wrong rows |
| A layer computes an invoice total or a refund | Test mandatory: unit tests in that layer for each rule and the rounding boundaries | Blast radius rises to money and Reversibility to costly |
| The repository layer adds the tenant ID to every query, and the endpoint tests use one tenant | Test mandatory: a repository test with records of two tenants that checks each query returns one tenant's rows | Blast radius rises to safety-or-legal and Detectability to never, because a missing filter leaks records without an error |
| No test runs through all the layers, each layer is tested with the layer below mocked, and a broken seam fails with an error that users report | Test minimally: one endpoint test through all layers for each main path | Detectability moves to same-day |
| An assistant writes a mocked unit test for each layer in seconds | Do not add the per-layer tests | Test cost falls to trivial, but Detectability stays immediately and Reversibility trivial |
What breaks if you don't test
Without per-layer tests, branches that no endpoint test takes go unchecked. An endpoint test that lists five shipments runs one path through the query's filters and the service's rules. A shipment on customs hold that the query drops passes the endpoint test, and the customer, who sees a plausible list, notices days later.
What you lose if you over-test
Mocked tests for every layer check one behaviour three times: adding a response field means editing three mocked tests plus the endpoint test. Each mocked test asserts the call its own author expected, so it stays green when two layers disagree about a page number. A repository test with a mocked database never runs the SQL, where repository bugs sit.
What to do instead
- Write one endpoint test for each main path and each error response, through all layers against a real database, with a distinct value in each field. Spring MockMvc runs it without a deployed server.
- Unit test rules in the layer that holds them, usually the service layer, one case per branch and boundary.
- Test hand-written queries against a real database, with rows that each filter must leave out.
- Add no test for a layer that only forwards calls. The Practical Test Pyramid splits unit and integration tests along the same lines.
When the answer changes
- The endpoint tests run nightly instead of on each pull request, or do not exist.
- A layer holds rules or queries with more cases than the endpoint tests run.
- A layer computes money or filters records by tenant or owner.
Real incident + Code example
Page one that showed page two
On a logistics backend I worked on, each layer of the shipments list had unit tests with the layer below mocked. The controller passed ?page=1 to the service as 1. The service author assumed a zero-based number and passed 1 to Spring Data's PageRequest.of, which counts pages from zero. Both tests were green. In production, page one skipped the 20 newest shipments, and a customer with 12 shipments saw an empty list and wrote to support the next morning. We added one endpoint test with 25 shipments and deleted the two mocked tests:
// Controller test, service mocked: green
verify(shipmentService).list(customerId, 1, 20);
// Service test, repository mocked: green
verify(shipmentRepository)
.findByCustomerIdOrderByCreatedAtDesc(customerId, PageRequest.of(1, 20));
// Endpoint test through all layers, real database: fails on the bug
@Test
void firstPageStartsWithNewestShipment() throws Exception {
insertShipments(customerId, 25); // S-01 is the oldest, S-25 the newest
mockMvc.perform(get("/customers/{id}/shipments?page=1", customerId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items.length()").value(20))
.andExpect(jsonPath("$.items[0].reference").value("S-25"));
}
Related questions
FAQ
- Is it necessary to unit test every layer of an n-tier architecture?
No, a layer that only forwards calls needs no unit test when an endpoint test runs through it against a real database. Unit test the layers that hold rules.
- Should you write integration tests at every level?
No, one integration test per main path through all layers covers the wiring between them. Add a narrower one for a repository query whose filters the endpoint tests do not tell apart.
- Should I mock the layer below when I unit test a layer?
Replace the layer below only in a unit test of rules, such as a service that computes due dates, with an in-memory fake repository. A test that only verifies the call to the layer below stays green when the two layers disagree about an argument.
- Which layer should I test in a layered architecture?
Test each behaviour in the layer that holds it: rules in the service layer, queries in the repository layer, and routes and serialization through an endpoint test. A layer with none of these needs only the endpoint test.