Bonus Session - Advanced Mocking and Verification
By the end of this bonus session, you will:
Prerequisites: Complete Module 2 (Dependency Injection) first
While manual test doubles work well, Mockito provides powerful features for complex scenarios:
Add Mockito dependency to your build file:
Maven:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
Gradle:
testImplementation(
'org.mockito:mockito-junit-jupiter:5.14.2'
)
@ExtendWith(MockitoExtension.class) // (#1:JUnit 5 extension for Mockito support)
class UserServiceTest {
@Mock // (#2:Create mock instances)
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks // (#3:Automatically injects @Mock dependencies into UserService)
private UserService userService;
@Test
void shouldCreateUserAndSendEmail() {
User user = new User("test@example.com");
when(userRepository.save(any(User.class))) // (#4:Stub the mock behavior)
.thenReturn(user);
userService.createUser(user);
verify(userRepository).save(user); // (#5:Verify method was called)
verify(emailService).sendWelcomeEmail("test@example.com");
}
}
class UserServiceTest {
@Test
void shouldCreateUserWithManualMocks() {
// Create mocks manually (alternative to annotations)
UserRepository mockRepo = mock(UserRepository.class); // (#1:Create mock)
EmailService mockEmail = mock(EmailService.class);
// Create service with mocks
UserService service = new UserService(mockRepo, mockEmail);
User user = new User("john@example.com");
when(mockRepo.save(any(User.class))).thenReturn(user); // (#2:Define behavior)
// Act
service.createUser(user);
// Assert
verify(mockRepo).save(user); // (#3:Verify interaction)
}
}
Use annotations for cleaner tests, manual creation for one-off scenarios
@Test
void stubbingExamples() {
// Return a value
when(userRepository.findById(1L))
.thenReturn(Optional.of(new User("john@example.com"))); // (#1:Return value)
// Return different values on consecutive calls
when(userRepository.count())
.thenReturn(5L)
.thenReturn(10L)
.thenReturn(15L); // (#2:Different returns each call)
// Throw an exception
when(userRepository.findById(-1L))
.thenThrow(new IllegalArgumentException("Invalid ID")); // (#3:Throw exception)
// Answer dynamically based on arguments
when(userRepository.findByEmail(anyString()))
.thenAnswer(invocation -> { // (#4:Dynamic response)
String email = invocation.getArgument(0);
return Optional.of(new User(email));
});
}
@Test
void argumentMatcherExamples() {
// Match any value of a type
when(userRepository.save(any(User.class))) // (#1:Any User object)
.thenReturn(new User("saved@example.com"));
// Match specific conditions
when(userRepository.findByEmail(argThat(email ->
email.endsWith("@company.com")))) // (#2:Custom matcher)
.thenReturn(Optional.of(new User("employee")));
// Match null or non-null
when(userRepository.save(isNull())).thenThrow(new IllegalArgumentException());
when(userRepository.save(isNotNull())).thenReturn(new User()); // (#3:Null checks)
// Match by equality
when(userRepository.findById(eq(42L))) // (#4:Exact match)
.thenReturn(Optional.of(new User("answer@universe.com")));
}
Rule: If using any matcher, ALL arguments must use matchers!
@Test
void shouldVerifyInteractions() {
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);
User user = new User("test@example.com");
service.createUser(user);
// Verify exact call
verify(mockRepo).save(user); // (#1:Was save() called with this user?)
// Verify number of calls
verify(mockRepo, times(1)).save(any()); // (#2:Exactly once)
// Verify no other interactions
verifyNoMoreInteractions(mockRepo); // (#3:No unexpected calls)
// Verify never called
verify(mockRepo, never()).delete(any()); // (#4:Never called)
// Verify call order
InOrder inOrder = inOrder(mockRepo);
inOrder.verify(mockRepo).save(user); // (#5:Order matters)
}
| Mode | Description |
|---|---|
times(n) |
Exactly n invocations |
never() |
Zero invocations (same as times(0)) |
atLeastOnce() |
At least one invocation |
atLeast(n) |
At least n invocations |
atMost(n) |
At most n invocations |
only() |
Only this method was called |
@ExtendWith(MockitoExtension.class)
class EmailServiceTest {
@Mock
private EmailSender emailSender;
@InjectMocks
private NotificationService notificationService;
@Captor // (#1:Declare captor with annotation)
private ArgumentCaptor<Email> emailCaptor;
@Test
void shouldSendCorrectEmail() {
notificationService.notifyUser("user@example.com", "Welcome");
// Capture the argument passed to emailSender
verify(emailSender).send(emailCaptor.capture()); // (#2:Capture during verify)
Email sentEmail = emailCaptor.getValue(); // (#3:Get captured value)
assertEquals("user@example.com", sentEmail.getTo());
assertEquals("Welcome", sentEmail.getSubject());
assertTrue(sentEmail.getBody().contains("Welcome"));
}
}
@Test
void shouldCaptureAllEmails() {
// Trigger multiple notifications
notificationService.notifyUser("user1@example.com", "Hello");
notificationService.notifyUser("user2@example.com", "Welcome");
notificationService.notifyUser("user3@example.com", "Goodbye");
// Capture all invocations
verify(emailSender, times(3)).send(emailCaptor.capture()); // (#1:Verify 3 calls)
// Get all captured values
List<Email> allEmails = emailCaptor.getAllValues(); // (#2:Get all captures)
assertEquals(3, allEmails.size());
assertEquals("user1@example.com", allEmails.get(0).getTo());
assertEquals("user2@example.com", allEmails.get(1).getTo());
assertEquals("user3@example.com", allEmails.get(2).getTo());
}
class SpyVsMockTest {
@Mock
private List<String> mockList; // Completely fake - no real behavior
@Spy
private List<String> spyList = new ArrayList<>(); // Real object, partial mock
@Test
void demonstrateDifference() {
// Mock - all methods return defaults (null, 0, false, empty)
mockList.add("one");
assertEquals(0, mockList.size()); // (#1:Still 0! No real behavior)
// Spy - real methods called unless explicitly stubbed
spyList.add("one");
assertEquals(1, spyList.size()); // (#2:Actually adds element)
// Can stub specific methods on spy
when(spyList.size()).thenReturn(100); // (#3:Override specific method)
assertEquals(100, spyList.size()); // Returns 100
assertEquals("one", spyList.get(0)); // (#4:Real behavior still works)
}
}
@Test
void doReturnVsWhen() {
List<String> spy = spy(new ArrayList<>());
// WRONG: when() calls the real method first!
// when(spy.get(0)).thenReturn("element"); // Throws IndexOutOfBoundsException!
// CORRECT: doReturn() doesn't call the real method
doReturn("element").when(spy).get(0); // (#1:Safe for spies)
assertEquals("element", spy.get(0));
// For throwing exceptions
doThrow(new RuntimeException("Error"))
.when(spy).clear(); // (#2:Override void methods)
// For void methods that do nothing
doNothing().when(spy).clear(); // (#3:Suppress real behavior)
}
Rule: Always use doReturn() with spies to avoid calling real methods
import static org.mockito.BDDMockito.*;
@Test
void shouldCreateUserBDDStyle() {
// Given (setup)
User user = new User("test@example.com");
given(userRepository.save(any(User.class))) // (#1:BDD given - replaces when)
.willReturn(user);
// When (action)
User created = userService.createUser(user);
// Then (verification)
then(userRepository) // (#2:BDD then - replaces verify)
.should()
.save(user);
then(emailService)
.should(times(1))
.sendWelcomeEmail("test@example.com"); // (#3:Verification with mode)
assertThat(created.getEmail()).isEqualTo("test@example.com");
}
BDD style makes tests more readable with Given-When-Then structure
@Test
void shouldMockStaticMethods() {
try (MockedStatic<UUID> mockedUUID = mockStatic(UUID.class)) { // (#1:Static mock scope)
UUID fixedUUID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
mockedUUID.when(UUID::randomUUID) // (#2:Mock static method)
.thenReturn(fixedUUID);
// Test code that uses UUID.randomUUID()
String id = entityService.generateId();
assertEquals("123e4567-e89b-12d3-a456-426614174000", id);
} // (#3:Static mock is closed automatically - real behavior restored)
}
Warning: Static mocking is a code smell. Prefer dependency injection!
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.STRICT_STUBS) // (#1:Enable strict mode)
class StrictTest {
@Mock
private UserRepository userRepository;
@Test
void strictStubbingPreventsUnusedStubs() {
// This test will FAIL if you stub something you don't use
when(userRepository.findById(1L))
.thenReturn(Optional.of(new User("test"))); // (#2:Must be used!)
// If we don't call findById(1L), test fails with:
// "Unnecessary stubbings detected"
User user = userRepository.findById(1L).get(); // (#3:Stub is used - OK)
assertEquals("test", user.getEmail());
}
}
Strict stubbing helps identify dead code and copy-paste errors
// BAD: Mocking everything
when(user.getEmail()).thenReturn("test@example.com");
when(user.getName()).thenReturn("John");
// GOOD: Use real objects
User user = new User("test@example.com", "John");
// BAD: Verifying obvious calls
verify(userRepository).save(user);
verify(user).getEmail(); // Pointless!
// GOOD: Verify meaningful interactions
verify(emailService).sendWelcomeEmail("test@example.com");
Task: Write tests for an OrderService with mocks
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final NotificationService notificationService;
public Order placeOrder(Order order) {
Order saved = orderRepository.save(order);
paymentService.processPayment(order.getTotal());
notificationService.sendOrderConfirmation(order.getCustomerEmail());
return saved;
}
}
Requirements:
| Concept | Usage |
|---|---|
| @Mock | Create fake object with no behavior |
| @Spy | Wrap real object, override specific methods |
| @InjectMocks | Auto-inject mocks into class under test |
| when().thenReturn() | Define mock behavior |
| verify() | Check method was called |
| @Captor | Capture arguments for assertions |
Key Insight: Mocks are for isolation. Use them wisely, not everywhere!