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
| Need | Code |
|---|---|
| Stub return | when(x.foo()).thenReturn(v) |
| Stub exception | when(x.foo()).thenThrow(new E()) |
| Stub void method | doThrow(new E()).when(x).bar() |
| Verify call count | verify(x, times(2)).foo() |
| Never called | verify(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:
MockitoExtensionuses strict stubs: an unused stubbing fails the test, which catches dead setup.- Compare
BigDecimalwithisEqualByComparingTo, notequals(90.0and90.00are notequals). ArgumentCaptorlets 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
@SpringBootTestfor 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
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 TestcontainersFinished this page? Track your progress (saved only in your browser).