BLOG POSTS
Mockito Mock Examples – Unit Testing in Java

Mockito Mock Examples – Unit Testing in Java

Unit testing is a cornerstone of robust Java development, but testing code that depends on external services, databases, or complex objects can be challenging. Mockito simplifies this by allowing you to create mock objects that simulate real dependencies, making your tests faster, more reliable, and focused on the specific functionality you’re testing. This guide walks through practical Mockito mock examples, from basic mock creation to advanced testing scenarios, giving you the tools to write comprehensive unit tests that actually work in production environments.

Understanding Mockito’s Core Concepts

Mockito works by creating proxy objects that mimic the behavior of real dependencies. Instead of calling actual methods that might hit a database or external API, your tests interact with controlled mock objects that return predetermined responses. This approach isolates the unit under test and eliminates external dependencies that could cause flaky or slow tests.

The framework provides three main types of test doubles:

  • Mocks – Objects that verify interactions and can return stubbed responses
  • Spies – Partial mocks that wrap real objects while allowing selective method stubbing
  • Stubs – Objects that only provide predetermined responses without interaction verification

To get started, add Mockito to your project dependencies:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
</dependency>

Basic Mock Creation and Configuration

The simplest way to create mocks is using the @Mock annotation combined with MockitoExtension. Here’s a practical example testing a user service that depends on a repository:

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void shouldReturnUserWhenFound() {
        // Given
        Long userId = 1L;
        User expectedUser = new User(userId, "john@example.com", "John Doe");
        when(userRepository.findById(userId)).thenReturn(Optional.of(expectedUser));
        
        // When
        User actualUser = userService.getUser(userId);
        
        // Then
        assertThat(actualUser.getEmail()).isEqualTo("john@example.com");
        verify(userRepository).findById(userId);
    }
}

The @InjectMocks annotation automatically injects the mocked dependencies into your service under test. For scenarios where you need more control, create mocks programmatically:

@Test
void shouldHandleRepositoryException() {
    UserRepository mockRepository = mock(UserRepository.class);
    UserService service = new UserService(mockRepository);
    
    when(mockRepository.findById(any()))
        .thenThrow(new DatabaseException("Connection failed"));
    
    assertThrows(ServiceException.class, () -> service.getUser(1L));
}

Advanced Stubbing Techniques

Real-world scenarios often require more sophisticated mock behavior. Mockito provides several advanced stubbing options that handle complex interactions:

@Test
void shouldDemonstrateAdvancedStubbing() {
    // Sequential return values
    when(userRepository.findById(1L))
        .thenReturn(Optional.of(new User(1L, "first@call.com", "First")))
        .thenReturn(Optional.of(new User(1L, "second@call.com", "Second")));
    
    // Argument matching with custom logic
    when(userRepository.findByEmail(argThat(email -> email.endsWith("@admin.com"))))
        .thenReturn(Optional.of(new User(999L, "admin@admin.com", "Admin")));
    
    // Answer with custom logic
    when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
        User user = invocation.getArgument(0);
        user.setId(System.currentTimeMillis()); // Simulate ID generation
        return user;
    });
    
    // Void method stubbing
    doThrow(new ValidationException("Invalid data"))
        .when(userRepository).deleteById(eq(-1L));
}

For asynchronous operations, mock CompletableFuture returns:

@Test
void shouldHandleAsyncOperations() {
    AsyncUserService asyncService = mock(AsyncUserService.class);
    User user = new User(1L, "async@test.com", "Async User");
    
    when(asyncService.findUserAsync(1L))
        .thenReturn(CompletableFuture.completedFuture(user));
    
    CompletableFuture<User> result = asyncService.findUserAsync(1L);
    
    assertThat(result.join().getEmail()).isEqualTo("async@test.com");
}

Spy Objects for Partial Mocking

Spies are particularly useful when you want to test real object behavior while stubbing specific methods. This is common when testing legacy code or integration scenarios:

@Test
void shouldUseSpyForPartialMocking() {
    UserService realService = new UserService(userRepository);
    UserService spyService = spy(realService);
    
    // Use real method for validation
    // Stub only the external dependency call
    doReturn(Optional.of(new User(1L, "spy@test.com", "Spy User")))
        .when(userRepository).findById(1L);
    
    // This calls the real validation logic but uses mocked repository
    User result = spyService.getUser(1L);
    
    assertThat(result.getEmail()).isEqualTo("spy@test.com");
    verify(spyService).getUser(1L); // Verify the actual method was called
}

Be cautious with spies – they can lead to unexpected behavior if the real methods have side effects:

@Test
void shouldAvoidSpyPitfalls() {
    List<String> spyList = spy(new ArrayList<>());
    
    // Wrong - this calls the real add() method first
    // when(spyList.get(0)).thenReturn("mocked");
    
    // Correct - use doReturn for spies
    doReturn("mocked").when(spyList).get(0);
    
    assertThat(spyList.get(0)).isEqualTo("mocked");
}

Verification Patterns and Argument Capturing

Mockito’s verification capabilities go beyond simple method call checks. You can verify complex interactions and capture arguments for detailed assertions:

@Test
void shouldVerifyComplexInteractions() {
    // Test service that sends notifications
    NotificationService notificationService = mock(NotificationService.class);
    UserService userService = new UserService(userRepository, notificationService);
    
    User newUser = new User(null, "new@user.com", "New User");
    when(userRepository.save(any(User.class)))
        .thenReturn(new User(1L, "new@user.com", "New User"));
    
    userService.createUser(newUser);
    
    // Verify notification was sent with correct content
    ArgumentCaptor<NotificationMessage> messageCaptor = 
        ArgumentCaptor.forClass(NotificationMessage.class);
    
    verify(notificationService).sendNotification(messageCaptor.capture());
    
    NotificationMessage capturedMessage = messageCaptor.getValue();
    assertThat(capturedMessage.getRecipient()).isEqualTo("new@user.com");
    assertThat(capturedMessage.getSubject()).contains("Welcome");
    
    // Verify method call order
    InOrder inOrder = inOrder(userRepository, notificationService);
    inOrder.verify(userRepository).save(any(User.class));
    inOrder.verify(notificationService).sendNotification(any());
}

For timeout-sensitive operations, use verification with timing constraints:

@Test
void shouldVerifyAsyncCalls() {
    AsyncProcessor processor = mock(AsyncProcessor.class);
    
    processor.processAsync("test-data");
    
    // Verify the method was called within 2 seconds
    verify(processor, timeout(2000)).processAsync("test-data");
    
    // Verify it was called at least 3 times within 5 seconds
    verify(processor, timeout(5000).atLeast(3)).processAsync(anyString());
}

Testing REST Controllers and Web Layers

MockMvc integrates seamlessly with Mockito for testing web layers. Here’s a complete controller test example:

@WebMvcTest(UserController.class)
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    void shouldReturnUserJson() throws Exception {
        User user = new User(1L, "test@example.com", "Test User");
        when(userService.getUser(1L)).thenReturn(user);
        
        mockMvc.perform(get("/api/users/1")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.email").value("test@example.com"))
                .andExpect(jsonPath("$.name").value("Test User"));
        
        verify(userService).getUser(1L);
    }
    
    @Test
    void shouldHandleValidationErrors() throws Exception {
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"email\":\"invalid-email\"}"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.errors").exists());
        
        verify(userService, never()).createUser(any());
    }
}

Performance Considerations and Best Practices

Mock creation and verification can impact test performance, especially in large test suites. Here are optimization strategies:

Practice Impact Implementation
Use @MockBean sparingly High Creates new Spring context, slowing tests significantly
Reset mocks between tests Medium Use @ExtendWith(MockitoExtension.class) for automatic reset
Avoid over-verification Medium Verify only behavior that matters to your test case
Use lenient() for strict stubbing Low Prevents unnecessary stubbing exceptions in complex setups

Here’s a performance-optimized test setup:

@ExtendWith(MockitoExtension.class)
class OptimizedUserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Mock 
    private NotificationService notificationService;
    
    private UserService userService;
    
    @BeforeEach
    void setUp() {
        // Initialize once per test method
        userService = new UserService(userRepository, notificationService);
        
        // Common stubbing for most tests
        lenient().when(notificationService.isEnabled()).thenReturn(true);
    }
    
    @Test
    void shouldCreateUserEfficiently() {
        // Specific stubbing only when needed
        when(userRepository.save(any())).thenReturn(new User(1L, "test", "test"));
        
        User result = userService.createUser(new User(null, "test", "test"));
        
        // Verify only critical interactions
        assertThat(result.getId()).isEqualTo(1L);
    }
}

Common Pitfalls and Troubleshooting

Several issues frequently trip up developers when working with Mockito. Here are the most common problems and their solutions:

Stubbing void methods incorrectly:

// Wrong - this won't compile
// when(mockService.deleteUser(1L)).thenThrow(new RuntimeException());

// Correct
doThrow(new RuntimeException()).when(mockService).deleteUser(1L);

// For void methods that should do nothing
doNothing().when(mockService).logActivity(any());

Final class mocking issues:

Mockito 2+ supports final class mocking, but you need to enable it:

# Create src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
mock-maker-inline

Static method mocking (Mockito 3.4+):

@Test
void shouldMockStaticMethods() {
    try (MockedStatic<LocalDateTime> mockedStatic = mockStatic(LocalDateTime.class)) {
        LocalDateTime fixedTime = LocalDateTime.of(2023, 1, 1, 12, 0);
        mockedStatic.when(LocalDateTime::now).thenReturn(fixedTime);
        
        // Your test code that calls LocalDateTime.now()
        String result = service.generateTimestamp();
        
        assertThat(result).contains("2023-01-01");
    } // Static mock automatically closed
}

Argument matcher mixing:

// Wrong - mixing matchers with actual values
// when(service.findUser(eq(1L), "john")).thenReturn(user);

// Correct - use matchers for all arguments or none
when(service.findUser(eq(1L), eq("john"))).thenReturn(user);

// Or use actual values for all
when(service.findUser(1L, "john")).thenReturn(user);

Integration with Testing Frameworks

Mockito works well with various testing frameworks and CI/CD pipelines. For JUnit 5 integration, ensure you’re using the correct extension:

@ExtendWith(MockitoExtension.class)
class JUnit5MockitoTest {
    // Test implementation
}

// For parameterized tests with mocks
@ExtendWith(MockitoExtension.class)
@ParameterizedTest
@ValueSource(strings = {"admin@test.com", "user@test.com"})
void shouldHandleMultipleEmails(String email, @Mock UserRepository repository) {
    when(repository.findByEmail(email)).thenReturn(Optional.of(new User()));
    // Test logic
}

For Spring Boot integration tests, consider your server infrastructure needs. Whether you’re running tests on a VPS or dedicated servers, ensure your CI/CD environment has sufficient resources for comprehensive test suites that include both unit and integration tests.

TestContainers work excellently alongside Mockito for hybrid testing approaches:

@Testcontainers
class HybridIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    
    @Mock
    private ExternalApiService externalApiService; // Mock external dependencies
    
    @Autowired
    private UserRepository userRepository; // Real database interaction
    
    @Test
    void shouldTestWithRealDatabaseAndMockedExternals() {
        // Use real database for data operations
        User savedUser = userRepository.save(new User("real@test.com", "Real User"));
        
        // Mock external API calls
        when(externalApiService.validateEmail(any())).thenReturn(true);
        
        // Test your service with mixed real and mocked dependencies
        assertThat(savedUser.getId()).isNotNull();
    }
}

The official Mockito documentation provides comprehensive API reference and advanced usage patterns. For teams adopting test-driven development, Mockito’s mock-first approach encourages better API design by forcing you to think about interfaces and dependencies before implementation.

These examples demonstrate Mockito’s flexibility in creating maintainable, fast-running unit tests that provide confidence in your code’s behavior without the complexity and fragility of integration tests for every scenario.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

Your email address will not be published. Required fields are marked