Unit testing services
Unit tests are the base of the pyramid — the many fast tests that check your logic in isolation, with no
Spring context at all. In a Spring app the prime target is your service classes, where the business
logic lives. This lesson writes real unit tests for Dakiya's ParcelService with JUnit 5 and Mockito,
verified by running them.
A unit test uses no Spring
The defining feature: a unit test does not start the Spring context. It creates the class under test directly, provides its dependencies as mocks, and tests its logic in complete isolation. No database, no web server, no auto-configuration — just plain objects and JUnit. This is why unit tests run in milliseconds and why you can have thousands of them. If a test needs Spring to run, it is not a unit test — it is a slice or integration test (later lessons).
The tools, both in spring-boot-starter-test:
- JUnit 5 — the test framework (
@Test, assertions, lifecycle). - Mockito — creates mock objects for dependencies.
- AssertJ — fluent assertions (
assertThat(x).isEqualTo(y)), the readable default.
Testing a service with Mockito
ParcelService depends on ParcelRepository. To test the service's logic without a real database, mock
the repository:
@ExtendWith(MockitoExtension.class) // enable Mockito
class ParcelServiceTest {
@Mock ParcelRepository repository; // a fake repository
@InjectMocks ParcelService service; // the real service, with the mock injected
@Test
void book_savesAParcelWithGeneratedTrackingCode() {
when(repository.save(any(Parcel.class))).thenAnswer(inv -> inv.getArgument(0)); // stub
Parcel p = service.book("Asha", "Pune");
assertThat(p.getRecipientName()).isEqualTo("Asha");
assertThat(p.getTrackingCode()).startsWith("PKG-");
assertThat(p.getStatus()).isEqualTo(Parcel.Status.BOOKED);
}
}
The pieces:
@ExtendWith(MockitoExtension.class)wires Mockito into the test.@Mockcreates a fakeParcelRepository— it has no real behaviour until you tell it what to do.@InjectMockscreates the realParcelServiceand injects the mock into it (via the constructor — another payoff of constructor injection from the DI lesson: the service is trivial to instantiate with a fake).when(...).thenReturn(...)(orthenAnswer) stubs the mock — defines what it returns when called, so you control the service's inputs precisely.
Verified by running: this test passes — the service generated a PKG- tracking code, kept the recipient,
and set status BOOKED, all without touching a database. That is the essence of a unit test: real logic,
fake dependencies, fast and isolated.
Stubbing and verifying interactions
Mockito does two jobs — stubbing (control what a dependency returns) and verifying (check the code called a dependency correctly):
// stubbing — control the input
when(repository.findByDestinationCity("Pune"))
.thenReturn(List.of(new Parcel("PKG-1", "Asha", "Pune")));
assertThat(service.inCity("Pune")).hasSize(1); // verified: passes
// verifying — assert an interaction happened
service.book("Asha", "Pune");
verify(repository).save(any(Parcel.class)); // the service DID call save exactly once
verify(repository, never()).delete(any()); // and never deleted
Stub when the code reads from a dependency (you need to control what it gets back); verify when the important behaviour is that the code calls a dependency (a save happened, an email was sent). Do not over-verify — asserting every interaction makes tests brittle; verify the interactions that matter (the save happened) and assert on the result for the rest.
What to unit test, and what not to mock
Unit-test the logic that is yours and interesting:
- Business rules, calculations, decisions, transformations, state transitions.
- Edge cases and failure paths (invalid input, boundary values) — the highest-value tests.
And a discipline about mocking: mock your own collaborators (the repository, another service), but do not
mock what you are testing, and do not mock value objects. Mocking the class under test verifies nothing;
mocking a simple data object (a Parcel) is pointless — just create a real one. Mocks are for the
dependencies whose real behaviour you want to exclude (the database, an external service), not for the
logic you are checking.
A word on over-mocking: a test that mocks so heavily it only checks "the code called the mocks I set up in the order I expected" tests your mocking, not your logic. Keep unit tests focused on behaviour and result — given these inputs (some stubbed), the method produces this output or makes this key call. That is the test that catches a real bug in your logic.
Check your work
No Spring. A unit test does not start the context — it creates the class directly, mocks its dependencies, tests logic in isolation. Milliseconds; thousands feasible. Needs Spring → not a unit test.
The tools. JUnit 5 (@Test), Mockito (@Mock, @InjectMocks, when/verify), AssertJ
(assertThat) — all in spring-boot-starter-test.
Testing a service. @ExtendWith(MockitoExtension.class), @Mock the repository, @InjectMocks the
service (constructor injection makes this trivial), when(...).thenReturn(...) to stub. Verified: the
service's book logic tested with no database.
Stub vs verify. Stub what the code reads from a dependency; verify that it called a dependency that matters (a save happened). Do not over-verify — assert the result for the rest.
What to test / not mock. Test your own interesting logic and edge/failure cases; mock collaborators, not the class under test or plain value objects; avoid over-mocking (which tests the mocks, not the logic).
Practice
- Write a
ParcelServiceTestwith@Mockrepository and@InjectMocksservice; testbookproduces aPKG-code andBOOKEDstatus (reproduce the verified pass) — no database. - Stub
findByDestinationCityand testinCityreturns the stubbed list. - Use
verify(repository).save(...)to assert the service called save; addverify(..., never())for something it must not do. - Add a business rule to the service (e.g. reject a blank recipient) and unit-test both the success and the failure path.
- Deliberately over-mock (mock a
Parcel) and reason about why that test verifies nothing; replace with a real object. - Run the unit tests and note how fast they are compared with a
@SpringBootTest.
Official documentation
- Spring Boot — Testing (unit tests) — What
starter-testprovides. - Mockito documentation —
@Mock,when,verify. - AssertJ — Fluent assertions.
Next: testing controllers with MockMvc.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship