Mockito + JUnit5 深度集成详解
Mockito 和 JUnit5 是现代 Java 测试的黄金组合,提供了强大的单元测试能力。下面我将详细解析它们的集成使用,并通过完整示例代码展示最佳实践。
一、集成架构与核心优势
核心优势:
- 简洁注解:
@Mock
、@InjectMocks
自动管理依赖 - 生命周期集成:Mock 对象与测试方法生命周期同步
- 参数化扩展:结合 Mockito 进行复杂场景测试
- 异常处理:统一管理测试异常验证
二、Maven 依赖配置
<dependencies>
<!-- JUnit5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
<!-- Mockito Core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<!-- Mockito JUnit5 Integration -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<!-- AssertJ for Fluent Assertions -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.23.1</version>
<scope>test</scope>
</dependency>
</dependencies>
三、核心注解详解
1. @ExtendWith(MockitoExtension.class)
启用 Mockito 与 JUnit5 的集成
2. @Mock
创建模拟对象:
@Mock
private UserRepository userRepository;
3. @Spy
创建部分真实对象:
@Spy
private EmailService emailService;
4. @InjectMocks
自动注入依赖:
@InjectMocks
private UserService userService;
5. @Captor
创建参数捕获器:
@Captor
private ArgumentCaptor<User> userCaptor;
四、完整业务场景示例
业务模型
public class User {
private Long id;
private String username;
private String email;
private boolean active;
// Constructors, getters, setters
}
public enum UserStatus {
ACTIVATED, DEACTIVATED, PENDING
}
服务层
public interface UserRepository {
User save(User user);
Optional<User> findById(Long id);
List<User> findByStatus(UserStatus status);
}
public class EmailService {
public void sendActivationEmail(String email) {
// 实际发送邮件逻辑
}
}
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public User registerUser(String username, String email) {
User user = new User(username, email);
User savedUser = userRepository.save(user);
emailService.sendActivationEmail(savedUser.getEmail());
return savedUser;
}
public User activateUser(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("User not found"));
user.setActive(true);
return userRepository.save(user);
}
public List<User> getPendingUsers() {
return userRepository.findByStatus(UserStatus.PENDING);
}
}
五、测试类实现
基础测试类结构
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
// 测试用例将在这里实现
}
1. 用户注册测试
@Test
void registerUser_ValidInput_CreatesUserAndSendsEmail() {
// 准备测试数据
String username = "john_doe";
String email = "john@example.com";
User savedUser = new User(1L, username, email, false);
// 配置mock行为
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// 执行测试方法
User result = userService.registerUser(username, email);
// 验证结果
assertThat(result)
.isNotNull()
.extracting(User::getId, User::getUsername, User::getEmail)
.containsExactly(1L, username, email);
// 验证交互
verify(userRepository).save(any(User.class));
verify(emailService).sendActivationEmail(email);
verifyNoMoreInteractions(userRepository, emailService);
}
2. 用户激活测试
@Test
void activateUser_ExistingUser_ActivatesUser() {
// 准备测试数据
Long userId = 1L;
User inactiveUser = new User(userId, "inactive_user", "inactive@example.com", false);
User activeUser = new User(userId, "active_user", "active@example.com", true);
// 配置mock行为
when(userRepository.findById(userId)).thenReturn(Optional.of(inactiveUser));
when(userRepository.save(any(User.class))).thenReturn(activeUser);
// 执行测试方法
User result = userService.activateUser(userId);
// 验证结果
assertThat(result.isActive()).isTrue();
// 验证交互
verify(userRepository).findById(userId);
verify(userRepository).save(argThat(user ->
user.getId().equals(userId) && user.isActive()
);
}
3. 异常场景测试
@Test
void activateUser_NonExistingUser_ThrowsException() {
// 准备测试数据
Long userId = 999L;
// 配置mock行为
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// 执行并验证异常
assertThatThrownBy(() -> userService.activateUser(userId))
.isInstanceOf(UserNotFoundException.class)
.hasMessageContaining("User not found");
// 验证交互
verify(userRepository).findById(userId);
verifyNoInteractions(emailService);
verifyNoMoreInteractions(userRepository);
}
4. 参数捕获器使用
@Test
void registerUser_CapturesCorrectUserData() {
// 准备测试数据
String username = "captured_user";
String email = "captured@example.com";
// 配置mock行为
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
User user = invocation.getArgument(0);
return new User(1L, user.getUsername(), user.getEmail(), false);
});
// 执行测试方法
userService.registerUser(username, email);
// 捕获参数
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(userCaptor.capture());
// 验证捕获的参数
User capturedUser = userCaptor.getValue();
assertThat(capturedUser)
.extracting(User::getUsername, User::getEmail, User::isActive)
.containsExactly(username, email, false);
}
5. Spy 部分真实对象
@Test
void registerUser_WithSpyEmailService() {
// 使用Spy包装真实对象
EmailService realEmailService = new EmailService();
EmailService emailServiceSpy = spy(realEmailService);
// 重新创建UserService注入Spy
UserService userService = new UserService(userRepository, emailServiceSpy);
// 配置mock行为
User savedUser = new User(1L, "spy_user", "spy@example.com", false);
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// 执行测试方法
userService.registerUser("spy_user", "spy@example.com");
// 验证交互
verify(emailServiceSpy).sendActivationEmail("spy@example.com");
// 注意:实际邮件发送方法会被调用
// 如要阻止实际调用,可使用:
// doNothing().when(emailServiceSpy).sendActivationEmail(anyString());
}
6. BDD 风格测试 (Given-When-Then)
import static org.mockito.BDDMockito.*;
@Test
void getPendingUsers_BDDStyle() {
// Given
User pendingUser1 = new User(1L, "pending1", "p1@example.com", false);
User pendingUser2 = new User(2L, "pending2", "p2@example.com", false);
List<User> pendingUsers = Arrays.asList(pendingUser1, pendingUser2);
given(userRepository.findByStatus(UserStatus.PENDING))
.willReturn(pendingUsers);
// When
List<User> result = userService.getPendingUsers();
// Then
then(userRepository).should().findByStatus(UserStatus.PENDING);
assertThat(result)
.hasSize(2)
.extracting(User::getUsername)
.containsExactly("pending1", "pending2");
}
7. 参数化测试 + Mockito
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@ParameterizedTest
@CsvSource({
"john, john@example.com",
"alice, alice@company.org",
"bob, bob@gmail.com"
})
void registerUser_Parameterized(String username, String email) {
// 准备测试数据
User savedUser = new User(1L, username, email, false);
// 配置mock行为
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// 执行测试方法
User result = userService.registerUser(username, email);
// 验证结果
assertThat(result.getEmail()).isEqualTo(email);
// 验证交互
verify(userRepository).save(argThat(user ->
user.getUsername().equals(username) &&
user.getEmail().equals(email)
));
verify(emailService).sendActivationEmail(email);
}
六、高级技巧与最佳实践
1. 初始化方法优化
@BeforeEach
void setUp() {
// 复杂Mock初始化
when(userRepository.save(any(User.class)))
.thenAnswer(invocation -> {
User user = invocation.getArgument(0);
return new User(ThreadLocalRandom.current().nextLong(1, 100),
user.getUsername(),
user.getEmail(),
false);
});
}
2. 自定义 Answer 实现
@Test
void activateUser_WithCustomAnswer() {
// 自定义Answer保存调用记录
List<User> activatedUsers = new ArrayList<>();
when(userRepository.save(any(User.class)))
.thenAnswer(invocation -> {
User user = invocation.getArgument(0);
activatedUsers.add(user);
return user;
});
// 测试执行...
// 验证自定义Answer
assertThat(activatedUsers)
.hasSize(1)
.allMatch(User::isActive);
}
3. 超时验证
@Test
void highConcurrencyActivation() {
// 配置mock行为
when(userRepository.findById(anyLong())).thenReturn(Optional.of(new User()));
when(userRepository.save(any(User.class))).thenReturn(new User());
// 模拟并发调用
IntStream.range(0, 100).parallel().forEach(i -> {
userService.activateUser((long) i);
});
// 验证并发调用
verify(userRepository, timeout(1000).atLeast(99)).save(any(User.class));
}
4. Mockito + AssertJ 组合断言
@Test
void complexUserValidation() {
User result = userService.registerUser("test", "test@example.com");
assertThat(result)
.isNotNull()
.satisfies(user -> {
assertThat(user.getId()).isPositive();
assertThat(user.getUsername()).isEqualTo("test");
assertThat(user.getEmail()).contains("@");
});
verify(emailService).sendActivationEmail(endsWith("example.com"));
}
七、测试生命周期管理
八、常见问题解决方案
问题 | 解决方案 |
---|---|
NPE 未初始化 Mock | 添加 @ExtendWith(MockitoExtension.class) |
Mock 行为未生效 | 检查 Stubbing 顺序(先配置后执行) |
参数匹配器使用错误 | 避免在验证中使用原始值,改用 any() 系列方法 |
多次调用返回相同值 | 使用链式 thenReturn() 或 thenAnswer() |
静态方法模拟失败 | 升级 Mockito 4.x+ 使用 mockStatic |
集成 Spring 测试问题 | 使用 @MockBean 替代 @Mock |
九、企业级最佳实践
1. 测试命名规范
// 反模式
@Test
void test1() {}
// 最佳实践
@Test
void registerUser_WhenEmailInvalid_ThrowsValidationException() {}
@Test
void activateUser_WithExpiredToken_ReturnsErrorResponse() {}
2. 分层测试策略
class UserServiceTest { // 单元测试: Mock所有依赖
@Mock UserRepository repo;
@Mock EmailService email;
@InjectMocks UserService service;
}
@SpringBootTest
class UserServiceIntegrationTest { // 集成测试: 部分真实依赖
@Autowired UserService service;
@MockBean EmailService emailService;
}
@DataJpaTest
class UserRepositoryTest { // 持久层测试: 真实数据库
@Autowired UserRepository repo;
}
3. 测试代码结构
@Test
void businessOperation_SuccessScenario() {
// 第一阶段: Arrange - 准备测试数据和Mock行为
given(dependency.performOperation(any())).willReturn(expectedResult);
// 第二阶段: Act - 执行被测方法
ActualResult result = testSubject.executeBusinessOperation(input);
// 第三阶段: Assert - 验证结果和交互
assertThat(result).isEqualTo(expected);
then(dependency).should().performOperation(input);
}
十、总结
Mockito + JUnit5 核心价值:
- 简洁高效:注解驱动减少样板代码
- 深度集成:无缝结合现代测试特性
- 灵活验证:支持复杂交互验证场景
- 可读性强:BDD 风格提升测试可读性
- 扩展性强:支持自定义验证规则
最佳实践建议:
- 对每个测试类使用
@ExtendWith(MockitoExtension.class)
- 遵循 Given-When-Then 结构组织测试
- 结合 AssertJ 创建流畅断言
- 对核心业务逻辑保持 80%+ 覆盖率
- 为复杂领域对象创建自定义断言
- 在 CI/CD 流水线中强制执行测试
完整测试栈示例:
@ExtendWith(MockitoExtension.class)
class CompleteTestExample {
@Mock
private Dependency dependency;
@InjectMocks
private ServiceUnderTest service;
@Test
void fullFeatureTest() {
// Given
given(dependency.getData(anyString())).willReturn("mockData");
// When
Result result = service.process("input");
// Then
assertThat(result)
.isNotNull()
.hasFieldOrPropertyWithValue("status", SUCCESS)
.extracting(Result::getDetails)
.isInstanceOf(Details.class);
verify(dependency, times(1)).getData("input");
}
}
通过 Mockito 和 JUnit5 的深度集成,开发者可以构建健壮、可维护的测试套件,有效保障 Java 应用程序的质量和稳定性。