Verdict
Yes
Test every business rule in the service layer with unit tests that replace the repositories with in-memory fakes and fix the clock; do not write separate tests for service methods that only forward a call to a repository.
Why
- Blast radiususers
- Change frequencyregularly
- Detectabilityeventually
- Reversibilitywith-effort
- Test costmoderate
Test the service layer, because the business rules of a layered backend live there. The typical case is a Spring or ASP.NET Core service that loads records through repositories, applies rules such as delivery cutoffs, and saves the result. Blast radius is users and Change frequency is regularly, because customers see the result of a broken rule and rules change with most features. Detectability is eventually, because a broken rule returns a plausible status or date. Reversibility is with-effort, because wrong records need a repair script, and Test cost is moderate, because each test needs repository fakes, so rule R11 gives Test.
| When | Decision | Why |
|---|---|---|
| The service method only forwards one value to a repository method, and controller or integration tests run it with data that tells that method apart from its siblings | Do not test the service method; test the code that calls it | Change frequency falls to rarely, Detectability to immediately, and Reversibility and Test cost to trivial, because a wrong call fails the callers' tests |
| The service calculates a charge, a refund or an invoice total | Test mandatory: the main path, every known failure and the boundary amounts | Blast radius rises to money and Reversibility to costly |
| The service decides whether the caller may read or change a record | Test mandatory, including calls that the service must deny | Blast radius rises to safety-or-legal and Detectability to never, because a missing check raises no error |
| Only staff use the service, through an internal back-office tool | Test minimally: one test of the main path of each rule | Blast radius falls to internal and Detectability to same-day, because staff report a wrong result the day they see it |
| HTTP-level tests already run every rule of the service with data that tells the outcomes apart | Do not add separate unit tests for the service | Detectability moves to immediately and Reversibility to trivial, because a broken rule fails CI before merge |
| The rules run in stored procedures that tests can reach only through a shared staging database | Test it differently: a nightly query counts records that break the rule and alerts above zero | Test cost rises to heavy while Detectability stays eventually and Reversibility with-effort |
What breaks if you don't test
A broken service rule throws no exception. A reversed comparison approves an order that should wait for stock, and the endpoint still returns 200, so controller tests with a mocked service and error alerts stay quiet. Customers notice days later, and every record saved under the wrong rule needs repair.
What you lose if you over-test
The common over-test gives every service method a mocked repository and verify() checks that restate the method body. Renaming one repository method then breaks every test that stubs it, and none of those tests checks a rule.
How to test
Write unit tests for each rule: one for the main path and one for each boundary likely to break, such as the minute of a cutoff. Replace repositories with in-memory fakes that hold real objects, and inject the clock through a seam, as the Microsoft unit testing guidance shows for DateTime.Now; in Java, pass a Clock to LocalTime.now(Clock). Assert the returned value or the saved state instead of repository calls. Test the service's custom queries separately against a real database, with the Spring Boot test slices or their equivalent.
When the answer changes
- The service moves money or decides who may see a record.
- The service only forwards calls to repositories, and controller or integration tests run it with data that tells each repository method apart from its siblings.
- The rules run in stored procedures that tests cannot reach.
Real incident + Code example
The cutoff that read the server's clock
On a grocery delivery backend I worked on, SlotService hid same-day slots after each store's 11:00 order cutoff. A refactor replaced ZonedDateTime.now(store.zone()) with LocalTime.now(), which reads the server's default time zone. Our servers ran in UTC, so in summer the Madrid stores kept same-day slots open until 13:00 local time. The service had no tests, and the controller tests mocked it. Over nine days customers booked 312 same-day slots the warehouse could not pick in time, and support called each customer to move the delivery. The fix injected a Clock, and the regression test runs it in UTC, as production does:
class SlotServiceTest {
// Production runs in UTC; 09:30 UTC is 11:30 in Madrid in July
Clock utc = Clock.fixed(Instant.parse("2026-07-01T09:30:00Z"), ZoneOffset.UTC);
InMemorySlotRepository slots = new InMemorySlotRepository();
SlotService service = new SlotService(slots, utc);
Store madrid = new Store("store-7", ZoneId.of("Europe/Madrid"), LocalTime.of(11, 0));
@Test
void hidesSameDaySlotsAfterTheStoresLocalCutoff() {
slots.add(new Slot(madrid, LocalDate.of(2026, 7, 1), LocalTime.of(18, 0)));
slots.add(new Slot(madrid, LocalDate.of(2026, 7, 2), LocalTime.of(10, 0)));
List<Slot> offered = service.availableSlots(madrid);
assertEquals(1, offered.size());
assertEquals(LocalDate.of(2026, 7, 2), offered.get(0).date());
}
}
A service that reads LocalTime.now(clock) without the store's zone sees 09:30, offers the same-day slot, and fails the test.
Related questions
FAQ
- Should I write unit tests for the controller or the service layer?
Write unit tests for the service layer, where the business rules live, and give each controller endpoint one HTTP-level test. The controller test checks routing, binding and JSON; the service tests check the rules.
- Should I mock the repository when testing a service?
Yes, replace the repository in a service unit test, preferably with an in-memory fake that stores real objects, and assert the saved state.
- Do I need to test domain services in domain-driven design?
Yes, a domain service holds rules that span several entities, so test each rule with unit tests on plain objects. A domain service usually needs no fakes, because it takes entities as arguments and returns a result.
- Should I test a service method that only calls the repository?
No, a service method that only forwards one value to a repository needs no unit test when controller or integration tests run it with data that tells that method apart from its siblings. Add a test when the method forwards several arguments of one type, such as two dates.