Tags: java, junit, mockito, testing, tdd, unit-testing
Level: intermediate
TL;DR: Best practices, annotations, stubbing, and verification patterns for isolated Java unit tests using JUnit 5 and Mockito.
JUnit 5 separates API (junit-jupiter-api) from runtime engine (junit-jupiter-engine).
Mockito creates dynamic test doubles to isolate classes under test from external dependencies.
@ExtendWith(MockitoExtension.class) initializes mocks and manages lifecycle in JUnit 5.
Mocks return default dummy values (null, 0, false, empty collection) unless stubbed.
Spies wrap real objects, delegating method calls unless explicitly stubbed.
when(...).thenReturn(...) defines deterministic behavior for mocked method invocations.
verify(mock, times(n)).method(...) asserts interaction count and arguments passed to collaborators.
Argument matchers like any() or eq() require all method arguments to use matchers.
ArgumentCaptor captures actual arguments passed to mocks for detailed assertions.
Fast execution and test isolation prevent flaky builds and database/network side effects.
Q: What is the difference between @Mock and @Spy?R: I use @Mock to create a complete dummy object with no real logic, while @Spy wraps an existing real instance, executing actual code unless I stub specific methods.
Q: How do you test a void method with Mockito?R: For side-effect verification, I use verify(mock).voidMethod(). If the void method must throw an exception or do nothing, I stub it using doThrow() or doNothing().
Q: Why prefer doReturn().when() over when().thenReturn() on spies?R: With when(spy.call()).thenReturn(), the real method executes once during stub definition. doReturn().when(spy).call() avoids calling the real logic during setup.
Mixing raw values and matchers in the same invocation (e.g. mock.call(any(), "raw") throws InvalidUseOfMatchersException).
Mocking value objects, DTOs, or standard library collections instead of instantiating real objects.
Overusing @Spy or partial mocks, which often signals high coupling or poor class design.
Asserting implementation details instead of verifiable state or interface boundaries.
Forgetting that Mockito cannot mock static methods, private methods, or final classes without mockito-inline.
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private PaymentClient paymentClient;
@Mock
private OrderRepository orderRepository;
@InjectMocks
private OrderService orderService;
@Captor
private ArgumentCaptor<Order> orderCaptor;
@Test
void shouldProcessOrderSuccessfully() {
Order inputOrder = new Order("ORD-1", 100.0);
when(paymentClient.authorize(anyString(), anyDouble())).thenReturn(true);
when(orderRepository.save(any(Order.class))).thenAnswer(i -> i.getArgument(0));
boolean result = orderService.process(inputOrder);
assertTrue(result);
verify(orderRepository).save(orderCaptor.capture());
assertEquals("PROCESSED", orderCaptor.getValue().getStatus());
verify(paymentClient, times(1)).authorize(eq("ORD-1"), eq(100.0));
}
@Test
void shouldHandlePaymentFailure() {
doThrow(new PaymentException("Network error"))
.when(paymentClient).authorize(anyString(), anyDouble());
assertThrows(PaymentException.class, () -> orderService.process(new Order("ORD-2", 50.0)));
verifyNoInteractions(orderRepository);
}
}
[ ] ArgumentCaptor vs ArgumentMatcher usage rules
[ ] BDDMockito syntax (given / then) vs standard syntax
[ ] Strict stubbing behavior in JUnit 5 MockitoExtension
[x] Basic annotations: @Mock, @InjectMocks, @Spy
[x] Assertion methods in org.junit.jupiter.api.Assertions