
在 Mockito 测试中,当模拟的异常被代码内部 catch 捕获且未向外传播时,无法直接断言异常发生;但可通过验证目标方法是否被调用,间接确认异常触发路径已执行。
在 mockito 测试中,当模拟的异常被代码内部 catch 捕获且未向外传播时,无法直接断言异常发生;但可通过验证目标方法是否被调用,间接确认异常触发路径已执行。
在单元测试中,我们常需验证业务逻辑对异常的响应行为(如重试、降级、日志记录等)。但当异常被静默捕获(如空 catch 块)且无可观测副作用时,传统断言(如 assertThrows)将失效——因为异常从未逃逸出被测方法。此时,核心思路应从“验证异常是否抛出”转向“验证异常触发条件是否满足”。
最可靠且符合测试契约的方式是:验证被模拟抛出异常的方法是否确实被调用。这基于一个关键前提:doThrow(...).when(mock).method() 的行为是确定的——只要该方法被执行,异常就必然被抛出(即使立即被捕获)。因此,verify(mock).method() 成为间接但强有力的证据。
以下是一个典型实践示例:
// 被测类
public class ServiceOrchestrator {
private final OptionalService optionalService;
public ServiceOrchestrator(OptionalService optionalService) {
this.optionalService = optionalService;
}
public void doFullOperation() {
try {
optionalService.blink(); // 可能抛出 IOException
} catch (IOException e) {
// 静默处理:无日志、无状态变更
}
}
}
// 测试类(JUnit 5 + Mockito)
@ExtendWith(MockitoExtension.class)
class ServiceOrchestratorTest {
@Mock
OptionalService mockOptionalService;
@InjectMocks
ServiceOrchestrator orchestrator;
@Test
void doFullOperation_shouldInvokeBlink_whenIOExceptionIsSimulated() {
// Arrange: 模拟 blink() 抛出 IOException
doThrow(IOException.class).when(mockOptionalService).blink();
// Act: 执行被测方法(异常将被内部捕获)
orchestrator.doFullOperation();
// Assert: 验证 blink() 方法确实被调用 → 证明异常触发路径已走通
verify(mockOptionalService).blink();
}
}
✅ 为什么这足够可信?
-
verify(mock).method()断言了方法调用发生,而doThrow确保该调用必然引发异常; - 即使异常被
catch吞没,调用本身仍是受控流程的关键节点; - 该验证与“异常是否被抛出”的语义高度一致,且不依赖实现细节(如是否加日志)。
⚠️ 注意事项与最佳实践:
-
避免空 catch 块:生产代码中静默吞掉
IOException通常是设计缺陷。建议至少添加log.warn("blink failed, proceeding anyway", e),以便测试可验证日志或监控指标; -
慎用
verifyNoInteractions()作为反向验证:它仅说明方法未被调用,不能反推异常未发生; -
若需更高置信度,可结合
ArgumentCaptor捕获异常实例(适用于自定义异常含上下文信息的场景),但对标准IOException无额外收益; - 优先重构被测代码:为静默异常处理添加可测试的副作用(如设置状态标志、调用回调),使测试更直接、更健壮。
总结而言,在异常被内部捕获的场景下,“验证方法调用”是 Mockito 生态中最简洁、最可靠、最符合测试原则的验证策略——它不试图窥探 JVM 异常栈,而是聚焦于受控行为是否按预期执行,这正是单元测试的本质所在。










