Mockito and JUnit 6 are the most powerful duo in the Java testing toolkit. Mockito handles mock creation, stubbing, and verification while JUnit 6 orchestrates the test lifecycle. Together they let you test any unit in complete isolation, regardless of how many dependencies it has. This guide covers every Mockito feature you need — from basic mocking to advanced argument captors, spies, and common pitfalls to avoid.
Setup: Mockito with JUnit 6
<!-- Mockito Core + JUnit 5/6 integration -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.12.0</version>
<scope>test</scope>
</dependency>
<!-- Note: spring-boot-starter-test already includes mockito-junit-jupiter -->
Activate Mockito’s annotation processing by adding @ExtendWith(MockitoExtension.class) to your test class:
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class) // enables @Mock, @InjectMocks, @Captor, @Spy
class PaymentServiceTest { }
Creating Mocks: @Mock vs Mockito.mock()
@ExtendWith(MockitoExtension.class)
class MockCreationTest {
// Annotation style — cleanest, preferred for fields
@Mock
private PaymentGateway paymentGateway;
@Mock
private NotificationService notificationService;
// Programmatic style — useful when you need a mock inside a method
@Test
void programmaticMockCreation() {
// Create a mock inside the test method
UserRepository mockRepo = Mockito.mock(UserRepository.class);
when(mockRepo.findById(1L)).thenReturn(Optional.of(new User(1L, "Alice")));
User found = mockRepo.findById(1L).orElseThrow();
assertEquals("Alice", found.getName());
}
}
Stubbing with when().thenReturn()
@ExtendWith(MockitoExtension.class)
class StubbingExamplesTest {
@Mock private PaymentGateway paymentGateway;
@InjectMocks private PaymentService paymentService;
@Test
@DisplayName("Successful payment returns a confirmation number")
void successfulPaymentReturnsConfirmationNumber() {
// Stub: when gateway.charge() is called with ANY double, return a confirmation
when(paymentGateway.charge(anyDouble()))
.thenReturn(new PaymentResult("CONF-12345", true));
PaymentResult result = paymentService.processPayment(99.99);
assertTrue(result.isSuccessful());
assertEquals("CONF-12345", result.getConfirmationNumber());
}
@Test
@DisplayName("Gateway failure propagates as PaymentException")
void gatewayFailureThrowsPaymentException() {
// Stub: throw an exception when the gateway is called
when(paymentGateway.charge(anyDouble()))
.thenThrow(new GatewayTimeoutException("Gateway timed out"));
assertThrows(PaymentException.class,
() -> paymentService.processPayment(99.99));
}
@Test
@DisplayName("Multiple calls can return different values")
void consecutiveCallsReturnDifferentValues() {
// First call returns PENDING, second returns COMPLETED
when(paymentGateway.checkStatus(anyString()))
.thenReturn(PaymentStatus.PENDING)
.thenReturn(PaymentStatus.COMPLETED);
assertEquals(PaymentStatus.PENDING, paymentGateway.checkStatus("TX-001"));
assertEquals(PaymentStatus.COMPLETED, paymentGateway.checkStatus("TX-001"));
}
}
Verification: verify() and verifyNoInteractions()
@Test
@DisplayName("Sending an order sends exactly one confirmation email")
void sendingOrderSendsExactlyOneConfirmationEmail() {
Order order = new Order(1L, "[email protected]", 99.99);
when(orderRepository.save(any())).thenReturn(order);
orderService.placeOrder("[email protected]", 99.99);
// verify: was the method called exactly once with this argument?
verify(emailService, times(1)).sendConfirmation("[email protected]");
// verify: method was NEVER called with any other email
verify(emailService, never()).sendConfirmation("[email protected]");
// verify: no other methods on emailService were called
verifyNoMoreInteractions(emailService);
}
@Test
@DisplayName("Placing an invalid order does not interact with any service")
void invalidOrderDoesNotInteractWithServices() {
// An order with negative total should be rejected before any service call
assertThrows(IllegalArgumentException.class,
() -> orderService.placeOrder("[email protected]", -1.0));
// Verify zero interactions with ALL mocks
verifyNoInteractions(orderRepository, emailService, paymentGateway);
}
ArgumentCaptor: Capture and Inspect Arguments
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
@ExtendWith(MockitoExtension.class)
class ArgumentCaptorTest {
@Mock private EmailService emailService;
@Captor private ArgumentCaptor<EmailMessage> emailCaptor;
@InjectMocks private OrderService orderService;
@Test
@DisplayName("Confirmation email contains correct order details")
void confirmationEmailContainsCorrectOrderDetails() {
when(orderRepository.save(any())).thenReturn(
new Order(42L, "[email protected]", 149.99, OrderStatus.PENDING));
orderService.placeOrder("[email protected]", 149.99);
// Capture the EmailMessage object that was passed to emailService.send()
verify(emailService).send(emailCaptor.capture());
EmailMessage capturedEmail = emailCaptor.getValue();
// Now assert on the captured argument’s fields
assertEquals("[email protected]", capturedEmail.getTo());
assertTrue(capturedEmail.getSubject().contains("Order #42"),
"Email subject should contain the order ID");
assertTrue(capturedEmail.getBody().contains("149.99"),
"Email body should contain the order total");
}
}
Spying: @Spy for Partial Mocking
import org.mockito.Spy;
@ExtendWith(MockitoExtension.class)
class SpyExamplesTest {
// @Spy wraps a REAL object: real methods are called unless explicitly stubbed
@Spy
private ArrayList<String> spiedList = new ArrayList<>();
@Test
@DisplayName("Spy: real methods called unless stubbed")
void spyCallsRealMethods() {
// Real method — actually adds to the list
spiedList.add("Hello");
spiedList.add("World");
// Real method — returns actual size
assertEquals(2, spiedList.size());
// Stub one method: override size() to return 99
doReturn(99).when(spiedList).size();
assertEquals(99, spiedList.size());
// add() still works as a real method
spiedList.add("Third");
verify(spiedList, times(3)).add(anyString());
}
@Test
@DisplayName("Spy on real service: stub only the external call")
void spyOnServiceStubsExternalCall() {
// Useful when the service has a mix of real logic and external calls
PaymentService realService = new PaymentService(paymentGateway);
PaymentService spy = Mockito.spy(realService);
// Stub only the external gateway call; real calculation logic runs normally
doReturn(new PaymentResult("CONF-99", true))
.when(spy).callExternalGateway(anyDouble());
PaymentResult result = spy.processPayment(50.00);
assertTrue(result.isSuccessful());
}
}
Argument Matchers Reference
| Matcher | Matches |
|---|---|
any() | Any object (including null) |
any(Type.class) | Any non-null instance of Type |
anyString() | Any non-null String |
anyInt(), anyDouble() | Any int / double |
eq(value) | Exactly equal to value |
contains("str") | String containing “str” |
startsWith("prefix") | String starting with prefix |
argThat(predicate) | Custom predicate match |
isNull() | null |
isNotNull() | Not null |
Frequently Asked Questions (FAQs)
Q1: When should I use @Mock vs @Spy?
Use @Mock when you want a completely fake object where all methods return defaults (null, 0, false) unless stubbed. Use @Spy when you want a real object but need to override specific methods. Spies are most useful when testing a class that has a mix of logic you want to keep real and external calls you want to stub out.
Q2: What is the difference between doReturn() and when().thenReturn()?
For @Mock objects, both work identically. For @Spy objects, always use doReturn().when(spy).method() rather than when(spy.method()).thenReturn(). The latter form actually calls the real method during stubbing setup (before you’ve defined the stub), which can cause unexpected side effects or exceptions.
Q3: What does @InjectMocks do and when does injection fail silently?
@InjectMocks creates an instance of the class under test and injects all @Mock and @Spy fields into it. Mockito tries constructor injection first, then setter injection, then field injection. If none work, the class is still created but mocks are not injected — with no error. To avoid this silent failure, prefer constructor injection in your production classes and verify your test output makes sense.
Q4: Can I mock final classes and static methods with Mockito?
Yes, since Mockito 4.x. Enable the MockMaker inline extension by creating a file at src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker containing mock-maker-inline. This enables mocking of final classes, final methods, and static methods via Mockito.mockStatic(). Note that inline mock-maker has some restrictions with Kotlin coroutines and certain bytecode patterns.
Q5: How do I verify the ORDER of mock interactions?
Use InOrder to verify that mocks were called in a specific sequence:
InOrder inOrder = inOrder(paymentGateway, emailService);
inOrder.verify(paymentGateway).charge(anyDouble()); // must happen first
inOrder.verify(emailService).sendConfirmation(anyString()); // must happen second
See Also
- JUnit 6 with Spring Boot: Unit, Slice, and Integration Testing
- Writing Your First Clean Test in JUnit 6 (Best Practices)
- JUnit 6 Assertions: All Methods Explained
- JUnit 6 Extensions Model: Build Custom Extensions
- JUnit 6 Tutorial: Complete Series Index
Conclusion
Mockito and JUnit 6 together cover every isolation need in a Java test suite. Use @Mock with when().thenReturn() for the majority of unit tests. Reach for ArgumentCaptor when you need to inspect complex arguments. Use @Spy for partial mocking of real objects. And always verify interactions to confirm your code calls its dependencies correctly — not just that it produces the right output.
Next: JUnit 6 with Testcontainers — replace H2 with a real database container and eliminate the most common source of integration test false positives.