Java Selenium+Cucumber下Extent Report生成后发送邮件解决方案
问题背景
当前技术栈为Java Selenium + Cucumber测试框架,基于extent properties配置与Spark模板生成Extent Report测试报告。
存在的时序问题:Extent Report完整写入磁盘需要约1分30秒,但配置的邮件发送逻辑在@After钩子中会提前触发,此时报告尚未生成完成,发送的附件是残缺文件。之前通过多处添加Thread.sleep()强制等待的方案无法适配生成时间波动,仍然存在时序竞争问题。
原有实现代码如下:
@After public void endTest(Scenario scenario) throws IOException, InterruptedException, AddressException, InvalidFormatException, ParseException, MessagingException { if (scenario.isFailed()) { try { //screenshot File sourcePath= ((TakesScreenshot)base.driver).getScreenshotAs(OutputType.FILE); byte[] fileContent = FileUtils.readFileToByteArray(sourcePath); scenario.attach(fileContent, "image/png", "image"); // ... and embed it in } catch (WebDriverException e) { e.printStackTrace(); } } else { try { File sourcePath= ((TakesScreenshot)base.driver).getScreenshotAs(OutputType.FILE); byte[] fileContent = FileUtils.readFileToByteArray(sourcePath); scenario.attach(fileContent, "image/png", "image"); } catch (Exception e) { e.printStackTrace(); } } Thread.sleep(30000); String zip = Zip.zip(new File("./Report")); BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream("C:\\Project\\Contract\\Report.zip"), 10000); byte[] decode = Base64.getDecoder().decode(zip); stream.write(decode); stream.close(); Thread.sleep(10000); Automail automail = new Automail(); automail.Gen_Reports(); Thread.sleep(30000); if(base.driver!=null) { base.driver.quit(); } }
问题根因
- 硬编码
Thread.sleep()属于不可靠的同步方案,测试用例数量变化、机器IO性能波动、磁盘负载高低都会改变Extent Report的实际生成时间,固定等待时长要么等待不足拿到残缺文件,要么等待过久拖慢测试执行效率 - 核心逻辑放错了钩子位置:Cucumber的
@After是每个测试场景执行完成后立即触发,而Extent Report的Spark模板是在所有测试场景执行完毕、调用extent.flush()后才会开始全量写入磁盘,在单场景后置钩子中等待,永远无法对齐整个测试套件的报告生成时机。
可靠实现方案
1. 拆分钩子职责
将不同生命周期的逻辑拆分到对应钩子中,不要全部堆在单场景@After里:
- 单场景
@After:仅负责场景截图挂载、WebDriver资源释放 - 全局套件后置钩子:Cucumber搭配JUnit5时用
@AfterAll、搭配JUnit4时用@AfterClass、搭配TestNG时用@AfterSuite,在这个钩子里处理Extent Report flush、报告生成状态检测、压缩、邮件发送逻辑,这个钩子会在所有测试场景全部执行完成后才触发,时机完全匹配报告生成流程。
2. 用文件状态轮询替代硬编码等待
实现轮询检测逻辑,只有同时满足三个条件才判定报告写入完成:
- Spark模板生成的报告入口文件(一般为
./Report/index.html)存在 - 文件大小连续3次轮询无变化,排除IO写入过程中的波动
- 文件可被正常读取,确认没有被写入进程加锁占用
同时设置最大超时时间避免无限等待,超时直接抛出异常告警。
3. 修正后完整代码
首先实现文件写入完成检测的工具方法:
/** * 等待目标文件写入完成 * @param reportFile 要检测的报告入口文件 * @param timeoutMs 最大等待超时时间,单位毫秒 * @param pollIntervalMs 轮询检测间隔,单位毫秒 * @return 写入完成返回true,超时返回false */ private static boolean waitForFileWriteComplete(File reportFile, long timeoutMs, long pollIntervalMs) throws InterruptedException { long startTime = System.currentTimeMillis(); long lastFileSize = -1; int stableCheckCount = 0; // 连续3次检测文件大小无变化,才判定写入稳定 int requiredStableTimes = 3; while (System.currentTimeMillis() - startTime < timeoutMs) { if (reportFile.exists() && reportFile.isFile()) { long currentSize = reportFile.length(); if (currentSize == lastFileSize) { stableCheckCount++; if (stableCheckCount >= requiredStableTimes) { // 验证文件可正常读取,无写入锁占用 try (FileInputStream ignored = new FileInputStream(reportFile)) { return true; } catch (IOException e) { // 文件被锁定,重置稳定计数重新检测 stableCheckCount = 0; } } } else { lastFileSize = currentSize; stableCheckCount = 0; } } Thread.sleep(pollIntervalMs); } return false; }
精简后的单场景@After钩子,移除所有硬编码等待和报告处理逻辑:
@After public void endTest(Scenario scenario) { // 统一处理截图,无需拆分失败/成功分支 try { if (base.driver != null) { File sourcePath = ((TakesScreenshot)base.driver).getScreenshotAs(OutputType.FILE); byte[] fileContent = FileUtils.readFileToByteArray(sourcePath); scenario.attach(fileContent, "image/png", "test_screenshot"); } } catch (Exception e) { e.printStackTrace(); } finally { // 及时释放WebDriver资源 if(base.driver != null) { base.driver.quit(); } } }
全局后置钩子处理报告、压缩、发邮件逻辑:
@AfterAll public static void afterAllTests() throws Exception { // 1. 手动触发Extent Report全量写入,替换为你项目中实际的ExtentReports实例 extent.flush(); // 2. 等待报告完全写入,最大等待3分钟,每2秒轮询一次 File sparkIndexFile = new File("./Report/index.html"); boolean reportReady = waitForFileWriteComplete(sparkIndexFile, 180000, 2000); if (!reportReady) { throw new RuntimeException("测试报告生成超时,超过3分钟未完成写入"); } // 3. 报告就绪后执行压缩 String zipBase64 = Zip.zip(new File("./Report")); try (BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream("C:\\Project\\Contract\\Report.zip"), 10000)) { byte[] decode = Base64.getDecoder().decode(zipBase64); stream.write(decode); } // 4. 压缩完成后触发邮件发送 Automail automail = new Automail(); automail.Gen_Reports(); }
注意事项
- 不要在单场景
@After钩子中调用extent.flush(),反复flush会大幅拖慢报告生成速度,仅在全局后置钩子中调用一次即可 - Windows环境下文件写入时默认加独占锁,必须保留文件可读取校验逻辑,否则可能出现文件大小稳定但仍被占用,压缩包损坏的问题
- 最大超时时间建议设置为日常报告生成耗时的2倍,比如日常1分30秒生成就设3分钟,足够覆盖偶发的IO波动场景
内容的提问来源于stack exchange,提问作者gaurav kumar
相关产品推荐
相关产品推荐

