Mocking Patterns
Overview
This guide covers advanced mocking patterns and best practices for unit testing in the Ink platform. Learn when to use mocks, stubs, and spies, and how to effectively isolate components for testing.
Target Audience: Developers writing unit tests
Prerequisites: Understanding of Unit Testing Guide
Estimated Time: 30-40 minutes
Prerequisites
- Completed Unit Testing Guide
- Familiarity with Mockito framework
- Understanding of dependency injection
- Knowledge of JUnit 5
Mocking Strategies
Installation Steps
1. Mockito Setup
<!-- filepath: /Users/jetstart/dev/jetrev/ink/pom.xml -->
<!-- ...existing code... -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
<!-- ...existing code... -->
2. Mock vs Spy Configuration
// filepath: /Users/jetstart/dev/jetrev/ink/src/test/java/com/jetrev/ink/patterns/MockingExamples.java
@ExtendWith(MockitoExtension.class)
class MockingExamples {
// Pure mock - all methods return default values
@Mock
private UserRepository userRepository;
// Spy - real object with ability to mock specific methods
@Spy
private UserValidator userValidator = new UserValidator();
// Mock with custom answer
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ComplexService complexService;
@InjectMocks
private UserService userService;
}
Configuration
Repository Mocking Patterns
// filepath: /Users/jetstart/dev/jetrev/ink/src/test/java/com/jetrev/ink/repository/RepositoryMockingTest.java
@ExtendWith(MockitoExtension.class)
class RepositoryMockingTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
@DisplayName("Mock repository findById - returns optional")
void mockFindById() {
// Given
User user = User.builder()
.id(1L)
.username("testuser")
.build();
when(userRepository.findById(1L))
.thenReturn(Optional.of(user));
// When
User found = userService.getUserById(1L);
// Then
assertThat(found).isNotNull();
assertThat(found.getUsername()).isEqualTo("testuser");
}
@Test
@DisplayName("Mock repository save - returns saved entity")
void mockSave() {
// Given
User inputUser = User.builder()
.username("newuser")
.build();
User savedUser = User.builder()
.id(1L)
.username("newuser")
.createdAt(Instant.now())
.build();
when(userRepository.save(any(User.class)))
.thenReturn(savedUser);
// When
User result = userService.createUser(inputUser);
// Then
assertThat(result.getId()).isNotNull();
assertThat(result.getCreatedAt()).isNotNull();
}
@Test
@DisplayName("Mock repository query methods")
void mockQueryMethods() {
// Given
List<User> users = Arrays.asList(
User.builder().username("user1").build(),
User.builder().username("user2").build()
);
when(userRepository.findByRolesContaining("ADMIN"))
.thenReturn(users);
// When
List<User> admins = userService.getAdminUsers();
// Then
assertThat(admins).hasSize(2);
}
}
Service Mocking Patterns
// filepath: /Users/jetstart/dev/jetrev/ink/src/test/java/com/jetrev/ink/service/ServiceMockingTest.java
@ExtendWith(MockitoExtension.class)
class ServiceMockingTest {
@Mock
private EmailService emailService;
@Mock
private NotificationService notificationService;
@InjectMocks
private RegistrationService registrationService;
@Test
@DisplayName("Verify service method calls")
void verifyServiceCalls() {
// Given
User user = User.builder()
.email("test@example.com")
.build();
// When
registrationService.registerUser(user);
// Then
verify(emailService).sendWelcomeEmail("test@example.com");
verify(notificationService).notifyAdmins(any());
verifyNoMoreInteractions(emailService);
}
@Test
@DisplayName("Mock void methods")
void mockVoidMethods() {
// Given
doNothing().when(emailService).sendEmail(anyString(), anyString());
// Or throw exception
doThrow(new EmailException("SMTP error"))
.when(emailService)
.sendEmail(eq("invalid@example.com"), anyString());
// When & Then
assertThatThrownBy(() ->
emailService.sendEmail("invalid@example.com", "test"))
.isInstanceOf(EmailException.class);
}
@Test
@DisplayName("Mock methods with callbacks")
void mockWithCallback() {
// Given
doAnswer(invocation -> {
String email = invocation.getArgument(0);
System.out.println("Sending email to: " + email);
return null;
}).when(emailService).sendEmail(anyString(), anyString());
// When
emailService.sendEmail("test@example.com", "Hello");
// Then
verify(emailService).sendEmail("test@example.com", "Hello");
}
}
External API Mocking
// filepath: /Users/jetstart/dev/jetrev/ink/src/test/java/com/jetrev/ink/integration/ExternalApiMockingTest.java
@ExtendWith(MockitoExtension.class)
class ExternalApiMockingTest {
@Mock
private RestTemplate restTemplate;
@InjectMocks
private StripeIntegrationService stripeService;
@Test
@DisplayName("Mock external API call")
void mockExternalApi() {
// Given
StripeResponse expectedResponse = new StripeResponse();
expectedResponse.setStatus("success");
when(restTemplate.exchange(
anyString(),
eq(HttpMethod.POST),
any(HttpEntity.class),
eq(StripeResponse.class)
)).thenReturn(ResponseEntity.ok(expectedResponse));
// When
StripeResponse response = stripeService.createCharge(100L);
// Then
assertThat(response.getStatus()).isEqualTo("success");
verify(restTemplate).exchange(
contains("stripe.com"),
eq(HttpMethod.POST),
any(),
eq(StripeResponse.class)
);
}
@Test
@DisplayName("Mock API failure scenarios")
void mockApiFailure() {
// Given
when(restTemplate.exchange(
anyString(),
any(HttpMethod.class),
any(),
eq(StripeResponse.class)
)).thenThrow(new RestClientException("Network error"));
// When & Then
assertThatThrownBy(() -> stripeService.createCharge(100L))
.isInstanceOf(IntegrationException.class)
.hasCauseInstanceOf(RestClientException.class);
}
}
Usage Examples
Using ArgumentCaptor
@Test
@DisplayName("Capture and verify complex arguments")
void captureComplexArguments() {
// Given
ArgumentCaptor<EmailRequest> captor =
ArgumentCaptor.forClass(EmailRequest.class);
User user = User.builder()
.email("test@example.com")
.username("testuser")
.build();
// When
registrationService.registerUser(user);
// Then
verify(emailService).sendEmail(captor.capture());
EmailRequest captured = captor.getValue();
assertThat(captured.getRecipient()).isEqualTo("test@example.com");
assertThat(captured.getTemplate()).isEqualTo("welcome");
assertThat(captured.getVariables())
.containsEntry("username", "testuser");
}
Stubbing Consecutive Calls
@Test
@DisplayName("Stub different responses for consecutive calls")
void stubConsecutiveCalls() {
// Given
when(userRepository.findById(1L))
.thenReturn(Optional.of(user))
.thenReturn(Optional.empty());
// First call
User first = userService.getUserById(1L);
assertThat(first).isNotNull();
// Second call
assertThatThrownBy(() -> userService.getUserById(1L))
.isInstanceOf(UserNotFoundException.class);
}
Spy Pattern for Partial Mocking
// filepath: /Users/jetstart/dev/jetrev/ink/src/test/java/com/jetrev/ink/patterns/SpyPatternTest.java
@ExtendWith(MockitoExtension.class)
class SpyPatternTest {
@Spy
private UserService userService;
@Mock
private UserRepository userRepository;
@Test
@DisplayName("Spy allows partial mocking")
void spyPartialMocking() {
// Given - mock only specific method
doReturn(true).when(userService).isValidUsername(anyString());
// Real method still works
User user = User.builder().username("test").build();
// When
boolean isValid = userService.isValidUsername("test");
// Then
assertThat(isValid).isTrue();
verify(userService).isValidUsername("test");
}
@Test
@DisplayName("Spy verifies real method calls")
void spyVerifyRealCalls() {
// Given
User user = User.builder().username("test").build();
// When - call real method
userService.validateUser(user);
// Then - verify internal method was called
verify(userService).validateEmail(user.getEmail());
verify(userService).validateUsername(user.getUsername());
}
}
Verification
Verification Modes
@Test
@DisplayName("Various verification modes")
void verificationModes() {
// Exact number of times
verify(userRepository, times(1)).save(any());
// At least once
verify(userRepository, atLeastOnce()).save(any());
// At most
verify(userRepository, atMost(3)).save(any());
// Never called
verify(emailService, never()).sendEmail(anyString(), anyString());
// Verify no interactions
verifyNoInteractions(notificationService);
// Verify no more interactions
verify(userRepository).save(any());
verifyNoMoreInteractions(userRepository);
}
Verification Order
@Test
@DisplayName("Verify invocation order")
void verifyInvocationOrder() {
// Given
InOrder inOrder = inOrder(userRepository, emailService);
// When
userService.registerUser(user);
// Then
inOrder.verify(userRepository).save(any(User.class));
inOrder.verify(emailService).sendWelcomeEmail(anyString());
}
Troubleshooting
Unnecessary Stubbing
// Problem: Stubbed but never used
when(userRepository.findById(999L)) // This is never called
.thenReturn(Optional.empty());
// Solution: Only stub what you use
when(userRepository.findById(1L))
.thenReturn(Optional.of(user));
userService.getUserById(1L); // Actually called
Stubbing Final Classes
// Requires mockito-inline dependency
@Mock
private FinalClass finalClass; // Works with mockito-inline
// Or use @Spy for real object
@Spy
private FinalClass finalClass = new FinalClass();
Mocking Static Methods
@Test
@DisplayName("Mock static methods")
void mockStaticMethods() {
try (MockedStatic<LocalDateTime> mockedStatic =
mockStatic(LocalDateTime.class)) {
LocalDateTime fixed = LocalDateTime.of(2024, 1, 1, 0, 0);
mockedStatic.when(LocalDateTime::now).thenReturn(fixed);
// Test code using LocalDateTime.now()
assertThat(LocalDateTime.now()).isEqualTo(fixed);
}
}
Code Examples
Custom Argument Matcher
// filepath: /Users/jetstart/dev/jetrev/ink/src/test/java/com/jetrev/ink/matchers/CustomMatchers.java
public class CustomMatchers {
public static User userWithEmail(String email) {
return argThat(user ->
user != null && email.equals(user.getEmail()));
}
public static CreateUserRequest validUserRequest() {
return argThat(request ->
request != null &&
request.getUsername() != null &&
request.getEmail() != null &&
request.getPassword() != null
);
}
}
// Usage
@Test
void testWithCustomMatcher() {
when(userRepository.save(userWithEmail("test@example.com")))
.thenReturn(savedUser);
}
Mock Builder Pattern
// filepath: /Users/jetstart/dev/jetrev/ink/src/test/java/com/jetrev/ink/builders/MockBuilder.java
public class MockBuilder {
public static UserRepository mockUserRepository() {
UserRepository mock = mock(UserRepository.class);
// Default behaviors
when(mock.save(any(User.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
when(mock.findById(anyLong()))
.thenReturn(Optional.empty());
return mock;
}
public static UserRepository mockUserRepositoryWithUser(User user) {
UserRepository mock = mockUserRepository();
when(mock.findById(user.getId()))
.thenReturn(Optional.of(user));
when(mock.findByUsername(user.getUsername()))
.thenReturn(Optional.of(user));
return mock;
}
}
Best Practices
- Mock External Dependencies Only: Don't mock what you own
- Use @InjectMocks Carefully: Ensure all dependencies are mocked
- Verify Important Interactions: Don't over-verify
- Avoid Stubbing Everything: Only stub what's needed
- Use Meaningful Test Data: Make tests readable
- Reset Mocks When Needed: Use
@BeforeEachorreset() - Prefer Composition Over Spies: Spies indicate design issues
- Mock at the Right Level: Mock repositories, not services
- Use ArgumentCaptor Sparingly: Only when necessary
- Keep Mocks Simple: Complex mocking indicates complex code
Performance Optimization
Reuse Mock Setup
@ExtendWith(MockitoExtension.class)
class OptimizedMockingTest {
@Mock
private UserRepository userRepository;
private User testUser;
@BeforeEach
void setUp() {
testUser = User.builder()
.id(1L)
.username("testuser")
.build();
// Common stubbing
when(userRepository.findById(1L))
.thenReturn(Optional.of(testUser));
}
@Test
void test1() {
// Reuses setup
User user = userRepository.findById(1L).get();
assertThat(user).isNotNull();
}
@Test
void test2() {
// Reuses setup
User user = userRepository.findById(1L).get();
assertThat(user.getUsername()).isEqualTo("testuser");
}
}
Related Documentation
Additional Resources
Next Steps: Learn about REST Controller Guide to implement testable controllers.