参数匹配器实战
参数匹配器(Argument Matchers)是Mockito实现灵活测试的核心工具,允许开发者通过模糊匹配或自定义条件验证方法参数。本章通过典型场景解析匹配器的使用技巧和常见陷阱。
1. 内置匹配器使用场景
Mockito提供丰富的内置匹配器,覆盖常见参数类型:
1.1 基础类型匹配
// 匹配任意字符串
when(userDao.findByUsername(anyString())).thenReturn(user);
// 匹配非空对象
when(orderService.createOrder(isNotNull())).thenReturn("ORDER_123");
// 匹配特定类型(忽略子类)
when(apiClient.send(any(JsonRequest.class))).thenReturn(response);
1.2 集合与复杂对象
// 匹配非空集合
verify(reportGenerator).generate(argThat(list -> !list.isEmpty()));
// 匹配Map包含特定键
when(configService.getConfig(argThat(map -> map.containsKey("timeout"))))
.thenReturn(defaultConfig);
1.3 混合精确与模糊匹配
// 第一个参数精确匹配,第二个参数任意整型
when(paymentService.charge(eq("user_001"), anyInt()))
.thenReturn(true);
// 注意:若方法有多个参数,使用匹配器时所有参数必须用匹配器!
2. 自定义参数匹配器
通过实现ArgumentMatcher
接口或使用Lambda表达式创建高级匹配逻辑。
2.1 实现ArgumentMatcher
接口
// 定义匹配年龄大于18岁的用户
public class AdultUserMatcher implements ArgumentMatcher<User> {
@Override
public boolean matches(User user) {
return user != null && user.getAge() > 18;
}
}
// 使用自定义匹配器
verify(userDao).save(argThat(new AdultUserMatcher()));
2.2 Lambda表达式简化
// 验证订单金额超过100且状态为PAID
verify(orderService).processOrder(argThat(order ->
order.getAmount() > 100 && order.getStatus() == OrderStatus.PAID
));
2.3 复用匹配器(提升可读性)
// 定义可重用的匹配器
public static ArgumentMatcher<User> hasValidEmail() {
return user -> user.getEmail().matches("^\\S+@\\S+\\.\\S+$");
}
// 在测试中使用
verify(emailService).sendWelcomeEmail(argThat(hasValidEmail()));
3. 匹配器组合技巧
3.1 集合元素验证
// 验证列表包含特定元素
verify(analyticsService).trackEvents(argThat(list ->
list.contains("purchase") && list.size() == 2
));
3.2 异步回调参数捕获
// 验证回调参数在异步操作后被修改
verify(asyncProcessor).setCompletionCallback(argThat(callback -> {
callback.onComplete(); // 触发回调逻辑
return true; // 始终匹配,实际测试中需结合状态验证
}));
3.3 忽略不重要的参数
// 只关心第一个参数,忽略时间戳
when(logService.write(eq("ERROR"), anyLong(), anyString()))
.thenReturn(true);
4. 常见错误与解决方案
错误场景 | 解决方案 |
---|---|
混合精确值和匹配器 | 所有参数必须使用匹配器:eq("fixed") 替代直接写值 |
过度使用any() 导致测试不严谨 | 关键参数使用精确匹配或条件约束 |
自定义匹配器中修改参数状态 | 保持匹配器无副作用,仅用于验证 |
匹配null 值混淆 | 明确使用isNull() 或nullable() (Mockito 2.1+) |
5. 匹配器性能优化
- 缓存复杂匹配器:
频繁使用的自定义匹配器应定义为静态常量。 - 避免深层嵌套对象匹配:
对复杂对象提取关键字段验证,而非全对象匹配。 - 优先使用内置匹配器:
例如anyList()
比argThat(list -> true)
更高效。
6. 实战案例:电商优惠券验证
@Test
void applyCoupon_ShouldVerifyComplexConditions() {
// 模拟优惠券服务
CouponService mockCouponService = mock(CouponService.class);
OrderService orderService = new OrderService(mockCouponService);
// 执行测试:应用优惠券
orderService.applyCoupon("USER_1001", "SUMMER2023");
// 验证:优惠券服务被调用,且参数满足:
// - 用户ID以"USER_"开头
// - 优惠券代码全大写
// - 使用时间为当天
verify(mockCouponService).validateCoupon(
argThat(userId -> userId.startsWith("USER_")),
argThat(code -> code.equals(code.toUpperCase())),
argThat(useTime -> useTime.toLocalDate().equals(LocalDate.now()))
);
}
总结
- 匹配器选择原则:
- 关键参数 → 精确匹配(
eq()
) - 非关键参数 → 模糊匹配(
any()
) - 复杂条件 → 自定义
argThat()
- 关键参数 → 精确匹配(
- 保持测试可读性:
- 为自定义匹配器定义有意义的名称
- 在验证失败时输出清晰的错误信息
通过合理使用参数匹配器,可以显著提升测试代码的适应性和可维护性,同时避免过度指定带来的脆弱性。接下来可学习Spy对象与部分Mock 进一步掌握精准控制技巧。