
本文详解如何为 try-catch 中静默处理异常(如日志记录后吞掉异常)的方法编写有效单元测试,重点解决 jacoco 覆盖率缺口,通过 mock 日志器并验证 error() 调用实现 100% 异常路径覆盖。
本文详解如何为 try-catch 中静默处理异常(如日志记录后吞掉异常)的方法编写有效单元测试,重点解决 jacoco 覆盖率缺口,通过 mock 日志器并验证 error() 调用实现 100% 异常路径覆盖。
在实际开发中,许多业务方法(如 sendSuccessEmail)采用“防御式设计”:内部调用可能失败的依赖(如邮件服务),但选择捕获所有 Exception 并仅记录错误,而非向上抛出。这类逻辑虽提升了系统健壮性,却给单元测试带来挑战——异常未逃逸出方法体,标准的 assertThrows 无法触发;而 Jacoco 等覆盖率工具会将 catch 块标记为未覆盖,导致覆盖率下降。
关键在于:我们不关心异常“是否发生”,而要验证 异常发生时,catch 块中的副作用是否按预期执行——最典型的就是日志记录。因此,测试策略应转向行为验证(Behavior Verification),而非状态断言。
✅ 正确做法:Mock 日志器并验证 error() 调用
假设您的类使用 SLF4J(如 Logger 接口),且日志器通过字段注入:
@Service
public class EmailService {
private static final Logger log = LoggerFactory.getLogger(EmailService.class);
public void sendSuccessEmail(Ar11ResultBean ar11ResultBean, String emailSmtpHost,
Integer emailSmtpPort, String emailAddressFrom, String emailAddressTo) {
try {
String emailBody = MessageFormat.format(Constants.EMAIL_BODY_SUCCESS,
ar11ResultBean == null ? "" : ar11ResultBean.getRowsProcessed());
sendEmail(emailSmtpHost, emailSmtpPort, emailAddressFrom, emailAddressTo,
Constants.EMAIL_SUBJECT_SUCCESS, emailBody);
} catch (Exception e) {
log.error("AR11 Application: Could not send success email due to error: ", e); // ← 待验证行为
}
}
// ... sendEmail() 实现(可能抛出 MessagingException 等)
}? 测试步骤(JUnit 5 + Mockito 5.x)
- Mock 日志器字段(需设为 @Mock 或使用 @InjectMocks 自动注入)
- Stub sendEmail() 抛出异常(触发 catch 分支)
- 调用被测方法
- 验证 log.error(...) 是否被精确调用一次,且消息前缀匹配
@ExtendWith(MockitoExtension.class)
class EmailServiceTest {
@Mock
private Logger logger; // 注意:需确保被测类中的 logger 字段可被注入或替换
@Mock
private EmailServiceImpl emailServiceImpl; // 假设 sendEmail 是其委托方法
@InjectMocks
private EmailService emailService;
@Test
void sendSuccessEmail_whenSendEmailThrowsException_thenLogsError() {
// Given: 配置 sendEmail 抛出异常
doThrow(new jakarta.mail.MessagingException("SMTP connection failed"))
.when(emailServiceImpl)
.sendEmail(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString());
// When: 执行被测方法(异常被捕获,无返回值)
emailService.sendSuccessEmail(
new Ar11ResultBean(), "smtp.example.com", 68, "from@test.com", "to@test.com"
);
// Then: 验证日志器被调用,且消息前缀正确(忽略异常对象本身,因 verify 不校验 varargs 第二参数)
verify(logger, times(1)).error(
eq("AR11 Application: Could not send success email due to error: ")
);
// ⚠️ 注意:SLF4J 的 error(String, Throwable) 是重载方法,Mockito 默认匹配第一个参数。
// 若需严格验证异常对象,可使用 ArgumentCaptor:
/*
ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
verify(logger).error(anyString(), captor.capture());
assertThat(captor.getValue()).isInstanceOf(MessagingException.class);
*/
}
}? 关键注意事项
-
日志器必须可 Mock:若 logger 是 static final(如 private static final Logger log = ...),则无法直接 mock。此时应:
- ✅ 推荐:改为非静态成员变量 + 构造器注入(符合测试友好原则);
- ⚠️ 次选:使用 mockito-inline + MockedStatic(仅当无法重构时):
try (MockedStatic<LoggerFactory> mocked = mockStatic(LoggerFactory.class)) { Logger mockLogger = mock(Logger.class); mocked.when(() -> LoggerFactory.getLogger(EmailService.class)).thenReturn(mockLogger); // ... 后续验证 }
- verify 匹配精度:log.error(String, Throwable) 的第二个参数是可变参数,verify 默认只校验第一个 String 参数。如需断言异常类型,务必使用 ArgumentCaptor。
- Jacoco 覆盖逻辑:此方案能 100% 覆盖 catch 块(语句覆盖 + 分支覆盖),满足 CI/CD 的质量门禁要求。
- 避免反模式:不要尝试“模拟异常被捕获”(如 when(Exception.class).then(...)),Mockito 不支持对 Exception 类型本身打桩;异常必须由被测方法内部真实抛出并捕获。
✅ 总结
测试 try-catch 静默处理逻辑的核心思想是:将“异常处理”视为一种可观测的行为(如日志、指标上报、状态更新),而非不可见的控制流。通过 Mock 外部协作对象(如邮件客户端、日志器),精准触发 catch 分支,并验证其副作用,即可实现高可靠、高覆盖率的单元测试。这不仅是技术实践,更是面向可观测性(Observability)的设计思维体现。

















