Mockito + AssertJ 深度集成指南:流畅测试的艺术
Mockito 和 AssertJ 是现代 Java 测试的完美组合,提供了模拟对象和流畅断言的强大能力。本文将深入探讨如何将两者无缝结合,创建可读性强、维护性高的测试代码。
一、核心优势与架构设计
核心优势:
- 表达力:链式调用使测试代码如自然语言般流畅
- 可读性:自描述断言降低理解成本
- 诊断信息:详细的错误报告加速问题定位
- 扩展性:支持自定义断言和匹配器
- 类型安全:减少运行时错误
二、Maven 依赖配置
<dependencies>
<!-- JUnit5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<!-- AssertJ -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.23.1</version>
<scope>test</scope>
</dependency>
</dependencies>
三、基础集成模式
1. 标准测试结构
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void registerUser_Success() {
// 1. Mock 行为设置
when(userRepository.save(any(User.class)))
.thenAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setId(1L); // 模拟数据库ID生成
return user;
});
// 2. 执行测试方法
User result = userService.registerUser("john", "john@example.com");
// 3. AssertJ 验证结果
assertThat(result)
.isNotNull()
.extracting(User::getId, User::getUsername, User::getEmail)
.containsExactly(1L, "john", "john@example.com");
// 4. Mockito 验证行为
verify(userRepository).save(any(User.class));
verifyNoMoreInteractions(userRepository);
}
}
2. BDD 风格测试
@Test
void registerUser_BDDStyle() {
// Given
given(userRepository.save(any(User.class)))
.willAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setId(1L);
return user;
});
// When
User result = userService.registerUser("alice", "alice@example.com");
// Then
then(userRepository).should().save(any(User.class));
assertThat(result)
.as("注册用户应返回有效用户对象")
.isNotNull()
.satisfies(user -> {
assertThat(user.getId()).isPositive();
assertThat(user.getUsername()).isEqualTo("alice");
assertThat(user.getEmail()).contains("@");
});
}
四、高级集成技巧
1. 集合断言与模拟验证
@Test
void findActiveUsers_ShouldReturnFilteredResults() {
// 准备测试数据
User activeUser = new User(1L, "active", "active@test.com", true);
User inactiveUser = new User(2L, "inactive", "inactive@test.com", false);
// 配置Mock行为
when(userRepository.findAll()).thenReturn(List.of(activeUser, inactiveUser));
// 执行测试
List<User> activeUsers = userService.findActiveUsers();
// AssertJ 集合断言
assertThat(activeUsers)
.hasSize(1)
.extracting(User::getUsername)
.containsExactly("active");
// Mockito 验证
verify(userRepository).findAll();
verifyNoMoreInteractions(userRepository);
}
2. 异常断言与行为验证
@Test
void activateUser_NonExistingUser_ThrowsException() {
// 配置Mock行为
when(userRepository.findById(999L)).thenReturn(Optional.empty());
// AssertJ 异常断言
assertThatThrownBy(() -> userService.activateUser(999L))
.isInstanceOf(UserNotFoundException.class)
.hasMessageContaining("User not found")
.hasNoCause();
// Mockito 验证
verify(userRepository).findById(999L);
verifyNoInteractions(userRepository);
}
3. 参数捕获与深度断言
@Captor
private ArgumentCaptor<User> userCaptor;
@Test
void registerUser_CapturesCorrectData() {
// 执行测试
userService.registerUser("bob", "bob@example.com");
// 捕获参数
verify(userRepository).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
// AssertJ 深度断言
assertThat(capturedUser)
.extracting(User::getUsername, User::getEmail, User::isActive)
.containsExactly("bob", "bob@example.com", false);
// 使用条件断言
assertThat(capturedUser).matches(user ->
user.getUsername().length() >= 3 &&
user.getEmail().contains("@")
);
}
4. 时间相关断言
@Test
void processOrder_ShouldCompleteInTime() {
// 配置慢速服务
when(paymentService.process(any())).thenAnswer(invocation -> {
Thread.sleep(50); // 模拟处理延迟
return true;
});
// 执行并验证性能
assertThatCode(() -> orderService.processOrder(new Order()))
.doesNotThrowAnyException()
.satisfies(result -> {
// 时间断言
assertThatExecutionTime()
.of(() -> orderService.processOrder(new Order()))
.isLessThan(100, TimeUnit.MILLISECONDS);
});
}
五、自定义扩展
1. 自定义 AssertJ 断言
public class UserAssert extends AbstractAssert<UserAssert, User> {
public UserAssert(User actual) {
super(actual, UserAssert.class);
}
public static UserAssert assertThat(User actual) {
return new UserAssert(actual);
}
public UserAssert hasValidEmail() {
isNotNull();
if (!actual.getEmail().contains("@")) {
failWithMessage("用户邮箱格式无效: %s", actual.getEmail());
}
return this;
}
public UserAssert isActive() {
isNotNull();
if (!actual.isActive()) {
failWithMessage("用户应处于激活状态");
}
return this;
}
}
// 使用自定义断言
@Test
void customAssertionUsage() {
User user = userService.activateUser(1L);
UserAssert.assertThat(user)
.isActive()
.hasValidEmail();
}
2. 自定义 Mockito Answer
@Test
void registerUser_WithCustomAnswer() {
// 自定义Answer收集数据
List<User> registeredUsers = new ArrayList<>();
when(userRepository.save(any(User.class)))
.thenAnswer(invocation -> {
User user = invocation.getArgument(0);
registeredUsers.add(user);
user.setId((long) registeredUsers.size());
return user;
});
// 执行测试
userService.registerUser("test", "test@example.com");
// AssertJ 验证自定义Answer
assertThat(registeredUsers)
.hasSize(1)
.first()
.extracting(User::getUsername, User::getEmail)
.containsExactly("test", "test@example.com");
}
六、最佳实践
1. 测试结构优化(AAA 模式)
@Test
void optimalTestStructure() {
// Arrange: 准备测试数据和Mock行为
User expectedUser = new User(1L, "arranged", "arranged@test.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(expectedUser));
// Act: 执行测试方法
User result = userService.getUser(1L);
// Assert: 使用AssertJ验证结果
assertThat(result)
.usingRecursiveComparison()
.ignoringFields("lastLogin")
.isEqualTo(expectedUser);
// Verify: 使用Mockito验证行为
verify(userRepository).findById(1L);
}
2. 断言优先顺序
3. 组合断言策略
@Test
void combinedAssertions() {
// 执行复杂操作
Report report = reportService.generateMonthlyReport(2023, 6);
// 组合断言
assertThat(report)
.as("2023年6月月度报告")
.isNotNull()
.satisfies(r -> {
// 财务数据验证
assertThat(r.getFinancialData())
.extracting(FinancialData::getRevenue, FinancialData::getProfit)
.containsExactly(BigDecimal.valueOf(150000), BigDecimal.valueOf(35000));
// 部门表现验证
assertThat(r.getDepartmentPerformance())
.filteredOn(dept -> dept.getRating() > 8)
.extracting(DepartmentPerformance::getName)
.containsExactly("Sales", "Engineering");
});
// 验证服务调用
verify(reportGenerator).generate(anyInt(), anyInt());
}
七、企业级应用场景
1. REST API 测试
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void getUserById_ShouldReturnUser() throws Exception {
// 准备Mock数据
User mockUser = new User(1L, "api_user", "api@test.com");
when(userService.getUser(1L)).thenReturn(mockUser);
// 执行请求
MvcResult result = mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andReturn();
// 提取响应
String json = result.getResponse().getContentAsString();
// AssertJ 验证JSON响应
assertThatJson(json)
.isObject()
.containsEntry("id", 1)
.containsEntry("username", "api_user")
.node("email").isString().contains("@");
}
}
2. 异步服务测试
@Test
void asyncProcessing_ShouldCompleteSuccessfully() {
// 配置异步Mock
when(asyncService.process(any()))
.thenReturn(CompletableFuture.completedFuture("success"));
// 执行测试
CompletableFuture<String> future = userService.asyncOperation("data");
// AssertJ 异步断言
assertThat(future)
.succeedsWithin(1, TimeUnit.SECONDS)
.isEqualTo("success");
}
3. 数据驱动测试
@ParameterizedTest
@CsvSource({
"john, john@example.com, true",
"invalid, bad-email, false",
"short, a@b.c, false"
})
void registerUser_ValidationCases(String username, String email, boolean valid) {
// 配置Mock
if (valid) {
when(userRepository.save(any())).thenReturn(new User(1L, username, email));
}
if (valid) {
User result = userService.registerUser(username, email);
assertThat(result).isNotNull();
} else {
assertThatThrownBy(() -> userService.registerUser(username, email))
.isInstanceOf(ValidationException.class);
}
}
八、性能优化
1. 共享 Mock 初始化
@BeforeEach
void setUp() {
// 公共Mock配置
when(userRepository.save(any(User.class)))
.thenAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setId(ThreadLocalRandom.current().nextLong(1, 1000));
return user;
});
}
2. 软断言收集所有错误
@Test
void multipleValidationsWithSoftAssertions() {
// 创建软断言对象
SoftAssertions softly = new SoftAssertions();
// 执行操作
User user = userService.registerUser("test", "test@example.com");
// 多个验证点
softly.assertThat(user.getId()).as("ID").isPositive();
softly.assertThat(user.getUsername()).as("Username").isEqualTo("test");
softly.assertThat(user.getEmail()).as("Email").contains("@");
softly.assertThat(user.isActive()).as("Active status").isFalse();
// 验证交互
verify(userRepository).save(any(User.class));
// 统一报告所有失败
softly.assertAll();
}
九、常见问题解决方案
问题 | 解决方案 |
---|---|
断言失败信息不足 | 使用 as() 添加描述性信息 |
集合内容验证困难 | 使用 extracting().containsExactly() |
复杂对象比较 | usingRecursiveComparison() 递归比较 |
Mock 行为未触发 | 检查方法签名匹配(参数、类型) |
异步测试超时 | 使用 succeedsWithin() 指定超时 |
嵌套属性验证复杂 | 自定义断言类封装验证逻辑 |
十、总结
Mockito + AssertJ 核心价值:
- 流畅表达:链式调用创建可读性强的测试代码
- 精准验证:结合模拟行为和结果断言全面覆盖
- 诊断友好:详细错误信息加速问题定位
- 扩展灵活:支持自定义断言和匹配器
- 现代语法:充分利用 Java 8+ 特性
最佳实践建议:
- 优先使用 BDD 风格(Given-When-Then)
- 为复杂领域对象创建自定义断言
- 使用软断言收集多个验证点错误
- 结合参数化测试覆盖边界条件
- 在持续集成中强制执行测试覆盖率
完整测试示例:
@ExtendWith(MockitoExtension.class)
class ComprehensiveTest {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentGateway paymentGateway;
@InjectMocks
private OrderService orderService;
@Test
void placeOrder_ComplexScenario() {
// Given: 准备测试数据
Order order = new Order(LocalDateTime.now(), List.of(
new OrderItem("A001", 2, 25.0),
new OrderItem("B002", 1, 100.0)
));
// 配置Mock行为
when(orderRepository.save(any())).thenAnswer(inv -> {
Order o = inv.getArgument(0);
o.setId(1001L);
return o;
});
when(paymentGateway.process(anyDouble())).thenReturn("PAY-12345");
// When: 执行测试方法
OrderResult result = orderService.placeOrder(order);
// Then: AssertJ 验证结果
assertThat(result)
.isNotNull()
.extracting(OrderResult::getOrderId, OrderResult::getStatus)
.containsExactly(1001L, OrderStatus.CONFIRMED);
assertThat(result.getPaymentId())
.as("支付ID格式")
.startsWith("PAY-")
.hasSizeGreaterThan(8);
// And: 验证交互行为
verify(paymentGateway).process(eq(150.0));
verify(orderRepository).save(same(order));
}
}
Mockito 和 AssertJ 的结合为 Java 测试提供了强大的表达能力和验证机制。通过遵循本文的模式和实践,您可以创建更健壮、更易维护的测试套件,显著提升软件质量和开发效率。