
本文详解如何为 try-catch 中静默处理异常(不 re-throw)的方法编写有效单元测试,重点通过 mock 日志、触发异常路径并验证日志调用,满足 jacoco 100% 行覆盖要求。
本文详解如何为 try-catch 中静默处理异常(不 re-throw)的方法编写有效单元测试,重点通过 mock 日志、触发异常路径并验证日志调用,满足 jacoco 100% 行覆盖要求。
在真实业务场景中,许多关键方法(如 sendSuccessEmail)采用“防御性设计”:内部调用可能失败的依赖服务(如邮件发送),但选择捕获异常并仅记录日志,而非向上抛出——这提升了系统健壮性,却给单元测试带来挑战:异常未逃逸,标准 assertThrows 失效;代码行被跳过,Jacoco 覆盖率告急。
此时,核心思路不是“模拟异常被抛出”,而是 “让异常真实发生 → 被 catch 块捕获 → 验证其副作用(如日志输出)”。由于 sendSuccessEmail 内部调用了 sendEmail,我们只需精准 mock sendEmail 的行为,使其在测试中抛出异常,从而驱动执行流进入 catch 分支。
✅ 正确做法:Mock 依赖方法抛出异常 + 验证日志调用
假设你的类结构如下(Spring Boot 环境常见):
@Service
public class EmailServiceImpl {
private final Logger log = LoggerFactory.getLogger(EmailServiceImpl.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);
}
}
// 可被 mock 的 protected 或 package-private 方法(便于测试)
void sendEmail(String host, Integer port, String from, String to, String subject, String body)
throws MessagingException {
// 实际邮件发送逻辑...
}
}
? 测试代码(JUnit 5 + Mockito)
@ExtendWith(MockitoExtension.class)
class EmailServiceImplTest {
@Mock
private Logger logger; // 注意:需使用 SLF4J 的 Logger 接口,非具体实现
@InjectMocks
private EmailServiceImpl emailService;
// 使用 Spy 替代直接 new,以便能 mock sendEmail 方法
@Spy
private EmailServiceImpl spyEmailService = new EmailServiceImpl();
@Test
void sendSuccessEmail_shouldLogErrorWhenSendFails() {
// Given: 准备测试数据
Ar11ResultBean resultBean = new Ar11ResultBean();
resultBean.setRowsProcessed(123);
String host = "smtp.example.com";
Integer port = 587;
String from = "from@example.com";
String to = "to@example.com";
// When: mock sendEmail 抛出 MessagingException(触发 catch)
doThrow(new MessagingException("SMTP connection failed"))
.when(spyEmailService)
.sendEmail(eq(host), eq(port), eq(from), eq(to), anyString(), anyString());
// 执行被测方法(不会抛出异常)
spyEmailService.sendSuccessEmail(resultBean, host, port, from, to);
// Then: 验证日志 error 方法被调用一次,且消息前缀匹配
verify(logger, times(1)).error(
"AR11 Application: Could not send success email due to error: ",
any(MessagingException.class)
);
}
}
⚠️ 关键注意事项:
- Logger 必须可 Mock:确保 log 字段是通过 LoggerFactory.getLogger(...) 获取的 SLF4J Logger 接口(非 Logback/Log4j 实现类)。若使用 Lombok @Slf4j,需改为手动注入 @Mock private Logger log; 并在 @BeforeEach 中通过 ((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(...)).setLevel(Level.ALL); 等方式重置——但更推荐直接 mock 接口。
- 避免 verify(logger).error(any()) 过度宽松:应校验消息字符串前缀(确保是目标日志)+ 异常对象(确保是捕获的真实异常),而非仅验证调用次数。
- sendEmail 方法需可被 Spy 拦截:将其设为 protected 或包级私有(package-private),否则 Mockito 默认无法 mock private/final 方法;若必须为 private,需启用 mockito-inline 并使用 MockedStatic(不推荐,增加复杂度)。
- Jacoco 覆盖原理:该测试强制执行了 catch 块内全部语句(日志调用),使 try 和 catch 分支均被标记为“已执行”,从而提升行覆盖率与分支覆盖率。
? 补充:若无法修改生产代码(如 sendEmail 为 private)
可采用 PowerMockito(慎用) 或更现代的 mockito-inline + MockedStatic(Mockito 4.0+)来 mock 静态/私有方法,但强烈建议优先重构为可测试设计(如提取 sendEmail 到独立 service 并注入),以保持测试简洁性与可维护性。
✅ 总结
测试「异常被捕获」逻辑的本质,是将异常视为输入信号,将日志/状态变更等副作用视为可验证输出。不要试图 when(Exception.class).then(...) —— Mockito 不提供此类语法,也不应需要。真正有效的方案永远是:
1️⃣ 精准触发异常路径(mock 依赖抛异常);
2️⃣ 聚焦验证可观测行为(日志、数据库状态、返回值等);
3️⃣ 确保测试稳定、快速、无副作用。
如此,你不仅能轻松满足 Jacoco 覆盖率指标,更能构建出真正守护业务健壮性的高质量单元测试。










