18.2 Mock Testing with Mockito
Mockito is Java's leading mocking framework. It lets you run unit tests in isolation by replacing real databases, external APIs, and file systems with fake objects (mocks).
1. Why Mocking?
Real test situation:
UserService → UserRepository → Database
Problems:
- Can't test without a DB connection
- Unstable results as DB state changes between tests
- Slow DB queries slow down the entire test suite
Solution: Replace DB with a mock object
UserService → MockUserRepository
- Always returns desired data
- Fast and stable
- No external dependencies
2. Setup
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
testImplementation 'org.mockito:mockito-core:5.5.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.5.0'
}
3. Basic Usage
// UserRepository.java
public interface UserRepository {
Optional<User> findById(Long id);
User save(User user);
boolean existsByEmail(String email);
}
// User.java
public record User(Long id, String name, String email, int age) {}
// UserService.java (class under test)
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) { this.userRepository = userRepository; }
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("User not found: " + id));
}
public User registerUser(String name, String email, int age) {
if (userRepository.existsByEmail(email))
throw new IllegalStateException("Email already in use: " + email);
return userRepository.save(new User(null, name, email, age));
}
}
// UserServiceTest.java
import org.junit.jupiter.api.*;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock // Auto-creates mock object
UserRepository userRepository;
@InjectMocks // Auto-injects mock into UserService
UserService userService;
@Test
@DisplayName("Get user by ID - success")
void getUserById_Success() {
// given: configure mock behavior (stub)
User mockUser = new User(1L, "Alice", "alice@example.com", 30);
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// when
User result = userService.getUserById(1L);
// then
assertNotNull(result);
assertEquals("Alice", result.name());
verify(userRepository).findById(1L); // verify mock was actually called
}
@Test
@DisplayName("Get non-existent user - throws exception")
void getUserById_NotFound() {
when(userRepository.findById(anyLong())).thenReturn(Optional.empty());
assertThrows(IllegalArgumentException.class, () -> userService.getUserById(999L));
}
@Test
@DisplayName("Register user - success")
void registerUser_Success() {
String email = "new@example.com";
User savedUser = new User(1L, "NewUser", email, 25);
when(userRepository.existsByEmail(email)).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(savedUser);
User result = userService.registerUser("NewUser", email, 25);
assertNotNull(result);
assertEquals(1L, result.id());
// Verify call order
InOrder inOrder = inOrder(userRepository);
inOrder.verify(userRepository).existsByEmail(email);
inOrder.verify(userRepository).save(any(User.class));
}
@Test
@DisplayName("Duplicate email - throws exception")
void registerUser_DuplicateEmail() {
when(userRepository.existsByEmail("existing@example.com")).thenReturn(true);
assertThrows(IllegalStateException.class, () ->
userService.registerUser("Alice", "existing@example.com", 30)
);
verify(userRepository, never()).save(any()); // save must NOT be called
}
}
4. Key Mockito Features
// ArgumentMatchers
when(repo.findById(anyLong())).thenReturn(Optional.of(user));
when(repo.save(any(User.class))).thenReturn(user);
when(repo.findById(argThat(id -> id > 0))).thenReturn(Optional.of(user));
// Throw exceptions
when(repo.findById(anyLong())).thenThrow(new RuntimeException("DB connection failed"));
// Multiple calls with different responses
when(repo.findById(anyLong()))
.thenReturn(Optional.of(user1)) // 1st call
.thenReturn(Optional.of(user2)) // 2nd call
.thenThrow(new RuntimeException()); // 3rd call
// Verify call counts
verify(repo, times(3)).findAll();
verify(repo, atLeast(1)).findByEmail(any());
verify(repo, never()).deleteById(any());
// Capture arguments
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(repo).save(captor.capture());
User saved = captor.getValue();
assertEquals("Alice", saved.name());
Pro Tip
Mock vs Stub vs Spy vs Fake:
| Type | Description | Mockito |
|---|---|---|
| Mock | Fake object that verifies expected calls | @Mock / mock() |
| Stub | Returns predefined values | when().thenReturn() |
| Spy | Real object with some methods stubbed | @Spy / spy() |
| Fake | Simple working implementation | Manual implementation |
Test Pyramid:
/\
/E2E\ ← Few (expensive, slow)
/------\
/Integration\ ← Medium
/-------------\
/ Unit Tests \ ← Many (fast, cheap)
/-----------------\
Unit tests form the base, isolated with Mockito. Integration/E2E tests use real DBs etc.
In Spring Boot, use @SpringBootTest, @WebMvcTest, @DataJpaTest for integration tests, and @MockBean to inject mocks into the Spring context.