Skip to main content
Spring Framework · Topic 13 of 25

Testing Basics: JUnit 5 and Mockito

spring-boot-starter-test gives you JUnit Jupiter, Mockito, AssertJ, Hamcrest, JSONassert and Spring Test. The key skill is choosing the cheapest test that gives confidence.

Business logic should be testable without starting Spring. Constructor injection makes this easy: just call new.

Definition​

A unit test checks one class on its own, without starting Spring. JUnit 5 runs the test and Mockito replaces the class's collaborators with fakes. Because Spring injects through constructors, you can simply pass the fakes in.

Why it exists​

Tests that start the whole application are slow, so people run them rarely and bugs live longer. Small isolated tests give feedback in milliseconds.

How it works​

Mockito cheatsheet​

NeedCode
Stub returnwhen(x.foo()).thenReturn(v)
Stub exceptionwhen(x.foo()).thenThrow(new E())
Stub void methoddoThrow(new E()).when(x).bar()
Verify call countverify(x, times(2)).foo()
Never calledverify(x, never()).foo()
Spy (partial mock)@Spy or spy(obj); prefer doReturn(...).when(spy).foo()

Example code​

A unit test without Spring​

Code under test:

public record Order(long id, BigDecimal total, boolean vip) { }

public interface DiscountPolicy {
BigDecimal discountFor(Order order);
}

public interface OrderRepository {
Order save(Order order);
}

public class CheckoutService {

private final DiscountPolicy discounts;
private final OrderRepository repository;

public CheckoutService(DiscountPolicy discounts, OrderRepository repository) {
this.discounts = discounts;
this.repository = repository;
}

public Order checkout(Order order) {
BigDecimal discount = discounts.discountFor(order);
if (discount.compareTo(order.total()) > 0) {
throw new IllegalStateException("Discount exceeds total");
}
Order finalOrder = new Order(order.id(), order.total().subtract(discount), order.vip());
return repository.save(finalOrder);
}
}

The test:

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class CheckoutServiceTest {

@Mock DiscountPolicy discounts;
@Mock OrderRepository repository;
@InjectMocks CheckoutService service;

@Test
void appliesDiscountBeforeSaving() {
Order in = new Order(1, new BigDecimal("100.00"), true);
when(discounts.discountFor(in)).thenReturn(new BigDecimal("10.00"));
when(repository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0));

Order saved = service.checkout(in);

assertThat(saved.total()).isEqualByComparingTo("90.00");
ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
verify(repository).save(captor.capture());
assertThat(captor.getValue().total()).isEqualByComparingTo("90.00");
}

@Test
void rejectsDiscountLargerThanTotal() {
Order in = new Order(2, new BigDecimal("5.00"), false);
when(discounts.discountFor(in)).thenReturn(new BigDecimal("6.00"));

assertThatThrownBy(() -> service.checkout(in))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("exceeds");

verifyNoInteractions(repository);
}
}

Points worth noting:

  • MockitoExtension uses strict stubs: an unused stubbing fails the test, which catches dead setup.
  • Compare BigDecimal with isEqualByComparingTo, not equals (90.0 and 90.00 are not equals).
  • ArgumentCaptor lets you assert on what was passed to a collaborator.

Parameterised tests​

@ParameterizedTest
@CsvSource({"0,false", "1,true", "-1,false"})
void isPositive(int value, boolean expected) {
assertThat(value > 0).isEqualTo(expected);
}

System design considerations​

  • Keep the pyramid: many unit tests, fewer slice tests, a handful of end-to-end tests
  • Make tests deterministic: control time, randomness and concurrency
  • Run the whole suite in CI on every change

When to use it, and when not​

Use it when

  • Logic can be tested without infrastructure
  • You want fast feedback in the inner development loop
  • You verify interactions with a collaborator that has side effects

Think twice when

  • You mock everything and assert on internal calls, which makes refactoring painful
  • The behaviour depends on real framework wiring (use a slice test)
  • You test getters and trivial code

Mock vs Spy vs Fake​

A mock has no real behaviour until stubbed. A spy wraps a real object. A fake is a simple working implementation (an in-memory repository). Fakes often give more robust tests than heavy mocking because they do not encode call sequences.

Common mistakes​

  • Over-mocking: verifying every internal call makes tests fail on harmless refactors.
  • Starting a @SpringBootTest for logic that has no Spring dependency.
  • Asserting only that "no exception was thrown".
  • Tests that depend on execution order.

Interview questions​

1. What is the difference between @Mock and @MockBean?

@Mock is Mockito only; no Spring context involved. @MockBean (Boot, deprecated in favour of Spring Framework's @MockitoBean from Boot 3.4) replaces or adds a bean in the Spring context. Using it forces context customisation and can slow the suite because contexts are cached per distinct configuration.

2. What does @InjectMocks do?

Creates the object under test and injects the @Mock fields through the constructor, then setters or fields.

3. How do you test private methods?

Do not. Test through the public API; if a private method is complex enough to need its own test, extract it into a collaborator.

4. How do you test a method that returns void?

Verify its side effects: interactions with mocks, state changes, or thrown exceptions.

5. What makes a test flaky and how do you avoid it?

Time, randomness, ordering, shared state, real network. Inject a Clock, seed random generators, isolate data per test, and use fakes or Testcontainers instead of shared environments.

Check yourself​

Quick quiz3 questions

1What is the main advantage of a unit test without Spring?

2When do you use a spy?

3What does verify(mock, times(1)).send(any()) check?

Key takeaways​

  • Most tests should be plain unit tests with constructor-injected collaborators.
  • Use Mockito for collaborators, AssertJ for readable assertions, parameterised tests for tables of cases.
  • Verify behaviour that matters, not implementation details.

Next: Slice tests and Testcontainers.

Next topicSlice tests and Testcontainers

Finished this page? Track your progress (saved only in your browser).