JUnit 5 assertThrows()无法捕获方法内部try-catch处理的异常,如何验证该异常已被抛出?
这个问题我之前踩过坑!当方法内部把异常捕获处理了,assertThrows确实抓不到,但咱们有几种可行的办法来验证异常确实被抛出过,分情况给你说说:
方案1:重构目标代码(最推荐,长期维护友好)
如果能修改SendEmail类的代码,这是最优解,调整后能让测试逻辑更清晰:
- 抽离独立方法单独测试:从栈跟踪能看到异常是
htmlToString方法抛出来的,要是把这个方法改成public(或者测试可见),直接测试它就行,assertThrows就能正常捕获异常了:
@Test void testHtmlToStringThrowsFileNotFound() { assertThrows(FileNotFoundException.class, () -> { SendEmail.htmlToString("fakeLocation"); }); }
- 让方法返回异常状态:修改
sendMail方法,让它返回一个包含异常信息的结果(比如Optional<Exception>),测试时就能直接检查这个结果:
// 修改后的SendEmail.sendMail方法 public static Optional<Exception> sendMail(String to, String subject, String filePath) { try { String htmlContent = htmlToString(filePath); // 原邮件发送逻辑 return Optional.empty(); } catch (FileNotFoundException e) { // 原异常处理逻辑(比如打日志) return Optional.of(e); } } // 对应的测试代码 @Test void testEmailSentHandlesFileNotFound() { Optional<Exception> result = SendEmail.sendMail(tenminuteMail, "testing", "fakeLocation"); assertTrue(result.isPresent()); assertTrue(result.get() instanceof FileNotFoundException); assertEquals("fakeLocation (The system cannot find the file specified)", result.get().getMessage()); }
- 提供测试专用的重载方法:如果业务不允许修改原有方法的行为,可以加一个重载方法,在测试场景下不捕获异常,直接抛出。
方案2:模拟依赖(适合异常来自外部资源的情况)
如果异常是因为文件读取这类外部操作导致的,你可以把文件读取逻辑封装成一个独立的服务类,然后用Mockito这类框架模拟这个服务的行为,验证它是否抛出了预期异常:
// 重构后的SendEmail类,依赖文件读取服务 public class SendEmail { private final FileContentReader fileReader; // 构造注入,方便测试时替换为mock public SendEmail(FileContentReader fileReader) { this.fileReader = fileReader; } public void sendMail(String to, String subject, String filePath) { try { String html = fileReader.readToString(filePath); // 邮件发送逻辑 } catch (FileNotFoundException e) { // 异常处理逻辑 log.error("Failed to read email template file: {}", filePath, e); } } } // 测试代码 @Test void testSendMailTriggersFileNotFound() { // 创建mock的文件读取服务 FileContentReader mockReader = Mockito.mock(FileContentReader.class); // 配置当传入fakeLocation时抛出异常 Mockito.when(mockReader.readToString("fakeLocation")) .thenThrow(new FileNotFoundException("fakeLocation")); SendEmail emailSender = new SendEmail(mockReader); emailSender.sendMail(tenminuteMail, "testing", "fakeLocation"); // 验证mock方法确实被调用过(说明异常触发了) Mockito.verify(mockReader).readToString("fakeLocation"); }
方案3:检查日志输出(无需改代码的折中方案)
如果你的SendEmail在捕获异常后会打印日志(比如用SLF4J、Log4j),可以用日志测试工具来验证日志中是否包含了目标异常的信息。比如用Logback的MemoryAppender:
@Test void testSendMailLogsFileNotFound() { // 配置MemoryAppender来捕获SendEmail类的日志 MemoryAppender logAppender = new MemoryAppender(); LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); Logger targetLogger = loggerContext.getLogger(SendEmail.class); logAppender.setContext(loggerContext); targetLogger.addAppender(logAppender); logAppender.start(); // 执行目标方法 SendEmail.sendMail(tenminuteMail, "testing", "fakeLocation"); // 验证日志中存在预期的异常信息 assertTrue(logAppender.contains("FileNotFoundException", Level.ERROR)); assertTrue(logAppender.contains("fakeLocation", Level.ERROR)); }
方案4:用PowerMock(仅当完全无法修改目标代码时)
如果完全不能改SendEmail的代码,这是最后的无奈之举——用PowerMock拦截底层的异常抛出逻辑。不过这种方式侵入性强,测试容易受代码变更影响,不推荐长期使用:
@RunWith(PowerMockRunner.class) @PrepareForTest(SendEmail.class) public class SendEmailTestClass { @Test void testEmailSentTriggersFileNotFound() throws Exception { // 拦截FileReader的构造方法,让它抛出异常 PowerMock.expectNew(FileReader.class, "fakeLocation") .andThrow(new FileNotFoundException("fakeLocation")); PowerMock.replay(FileReader.class); // 执行目标方法 SendEmail.sendMail(tenminuteMail, "testing", "fakeLocation"); // 验证异常确实被触发过 PowerMock.verify(FileReader.class); } }
总的来说,优先选方案1或2,它们能让代码更具可测试性,也符合良好的代码设计原则;方案3适合快速验证且不想改代码的场景;方案4尽量少用,除非万不得已。
内容的提问来源于stack exchange,提问作者Ryan Reddy
相关产品推荐
相关产品推荐

