RizTech Academy logo
RizTech Academy
TestingLesson 2 of 530 min

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.
  • @Mock creates a fake ParcelRepository — it has no real behaviour until you tell it what to do.
  • @InjectMocks creates the real ParcelService and 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(...) (or thenAnswer) 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

  1. Write a ParcelServiceTest with @Mock repository and @InjectMocks service; test book produces a PKG- code and BOOKED status (reproduce the verified pass) — no database.
  2. Stub findByDestinationCity and test inCity returns the stubbed list.
  3. Use verify(repository).save(...) to assert the service called save; add verify(..., never()) for something it must not do.
  4. Add a business rule to the service (e.g. reject a blank recipient) and unit-test both the success and the failure path.
  5. Deliberately over-mock (mock a Parcel) and reason about why that test verifies nothing; replace with a real object.
  6. Run the unit tests and note how fast they are compared with a @SpringBootTest.

Official documentation

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