
本文详解如何在java中基于gson手动构建cucumber兼容的json报告,并确保每个scenario生成一个独立、符合cucumber-report解析规范的json文件,避免重复写入与文件名冲突。
本文详解如何在java中基于gson手动构建cucumber兼容的json报告,并确保每个scenario生成一个独立、符合cucumber-report解析规范的json文件,避免重复写入与文件名冲突。
在自动化测试框架(尤其是混合式Hybrid Framework)中,将Cucumber测试执行结果导出为标准JSON格式,是集成cucumber-report、Allure或自定义可视化看板的关键前提。官方Cucumber JVM默认支持--plugin json:target/cucumber.json,但该方式仅生成单个聚合文件;而实际工程中常需按Scenario粒度拆分报告——例如用于CI/CD中按用例归档、失败用例快速定位、或对接支持单场景导入的分析平台。
然而,直接复用executionFinish(String filePath)多次调用当前实现会导致严重问题:每次调用都会遍历全部已添加的cucumberFeatures(即所有历史Scenario),并为每一个Feature生成一份文件,最终产生冗余(如cucumber_report1.jsonMy_Scenario.json和cucumber_report1.jsonAnother_Scenario.json同时存在)。根本原因在于cucumberFeatures是全局累积的集合,而executionFinish()未做“清空”或“切片”处理。
✅ 正确做法是:每个Scenario应封装为独立的Feature对象,并在添加完成后立即序列化为单独JSON文件,而非延迟批量处理。
以下是重构后的专业实践方案:
1. 核心设计原则
- 每个Scenario对应一个JsonObject(代表Cucumber Feature)
- addScenario() 初始化该Feature结构,addStep() 向其steps数组追加步骤
- saveScenarioAsJson(String fileName) 立即持久化当前Scenario,随后重置内部状态,准备下一个Scenario
2. 优化后的 CucumberJSONGenerator 关键代码
package com.demo.jdbc;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class CucumberJSONGenerator {
private JsonObject currentFeature;
private JsonArray currentElements;
private JsonObject currentElement;
private JsonArray currentSteps;
public CucumberJSONGenerator() {
reset();
}
private void reset() {
currentFeature = new JsonObject();
currentElements = new JsonArray();
currentElement = new JsonObject();
currentSteps = new JsonArray();
currentFeature.addProperty("name", "Unnamed Feature");
currentFeature.addProperty("keyword", "Feature");
currentFeature.add("tags", new JsonArray());
currentFeature.add("description", "");
currentFeature.add("line", 1);
currentElement.addProperty("name", "Unnamed Scenario");
currentElement.addProperty("keyword", "Scenario");
currentElement.addProperty("line", 2);
currentElement.add("steps", currentSteps);
currentElements.add(currentElement);
currentFeature.add("elements", currentElements);
}
public void addScenario(String scenarioName, String[] tags) {
// 重置前先保存上一个(若存在)
if (!currentSteps.isEmpty()) {
saveCurrentScenario();
}
reset();
currentElement.addProperty("name", scenarioName);
currentFeature.addProperty("name", "Feature: " + scenarioName); // 更语义化
JsonArray tagsArray = new JsonArray();
if (tags != null) {
for (String tag : tags) {
JsonObject tagObj = new JsonObject();
tagObj.addProperty("name", tag);
tagsArray.add(tagObj);
}
}
currentFeature.add("tags", tagsArray);
}
public void addStep(String stepName, String status) {
JsonObject step = new JsonObject();
step.addProperty("name", stepName);
step.addProperty("keyword", "Given"); // 可根据stepName前缀动态识别(Given/When/Then)
step.addProperty("line", currentSteps.size() + 3);
JsonObject result = new JsonObject();
result.addProperty("status", status.toLowerCase());
if ("failed".equalsIgnoreCase(status)) {
result.addProperty("error_message", "Step failed intentionally.");
}
step.add("result", result);
currentSteps.add(step);
}
/**
* 将当前Scenario保存为独立JSON文件,文件名基于场景名自动规范化
* @param baseDir 输出目录(如 "target/cucumber-reports/json")
*/
public void saveCurrentScenario(String baseDir) {
try {
File dir = new File(baseDir);
if (!dir.exists()) dir.mkdirs();
String safeName = currentElement.get("name").getAsString()
.replaceAll("[^a-zA-Z0-9\-_]", "_")
.replaceAll("_+", "_");
String fileName = String.format("%s_%d.json", safeName, System.currentTimeMillis());
File file = new File(dir, fileName);
Gson gson = new GsonBuilder().setPrettyPrinting().create();
JsonObject reportRoot = new JsonObject();
reportRoot.addProperty("name", "Cucumber Report");
reportRoot.addProperty("description", "Auto-generated per-scenario report");
JsonArray features = new JsonArray();
features.add(currentFeature);
reportRoot.add("features", features);
try (FileWriter writer = new FileWriter(file)) {
gson.toJson(reportRoot, writer);
}
System.out.println("✅ Saved scenario report: " + file.getAbsolutePath());
// 重置状态,为下一Scenario准备
reset();
} catch (IOException e) {
throw new RuntimeException("Failed to write JSON file", e);
}
}
// 便捷方法:使用默认目录
public void saveCurrentScenario() {
saveCurrentScenario("target/cucumber-reports/json");
}
}3. 调用示例(Main.java)
public class Main {
public static void main(String[] args) throws IOException {
CucumberJSONGenerator generator = new CucumberJSONGenerator();
// Scenario 1
generator.addScenario("My Scenario", new String[]{"@smoke", "@regression"});
generator.addStep("Given I navigate to the homepage", "passed");
generator.addStep("When I click the login button", "passed");
generator.addStep("Then I should see the dashboard", "failed");
generator.saveCurrentScenario(); // → target/cucumber-reports/json/My_Scenario_171xxxxx.json
// Scenario 2
generator.addScenario("Another Scenario", new String[]{});
generator.addStep("Given I open the application", "passed");
generator.addStep("When I enter my username", "passed");
generator.addStep("And I enter my password", "passed");
generator.addStep("Then I should be logged in", "passed");
generator.saveCurrentScenario(); // → target/cucumber-reports/json/Another_Scenario_171xxxxx.json
}
}⚠️ 关键注意事项
- 勿复用同一实例多次调用 executionFinish():原始逻辑缺陷根源在于“累加+全量遍历”,必须改为“构建→保存→重置”流水线。
- 文件名安全处理:使用正则替换空格、特殊符号(如replaceAll("[^a-zA-Z0-9\-_]", "_")),避免路径异常。
- 时间戳防重名:在文件名中加入System.currentTimeMillis(),确保并发或快速连续执行时不会覆盖。
- JSON Schema 兼容性:本实现严格遵循 Cucumber JSON schema —— 包含features[].elements[].steps[].result.status等必需字段,可被cucumber-report CLI 或 cucumber-html-reporter 正确解析。
- 资源清理:saveCurrentScenario() 内部自动reset(),无需手动干预生命周期。
✅ 总结
通过将“单Scenario建模”与“即时序列化”解耦,我们彻底规避了多文件交叉污染问题。此方案轻量、可控、可扩展——后续可轻松集成时间戳目录归档、JUnit/TestNG监听器钩子、或上传至S3/MinIO等存储服务。真正的工程化报告生成,始于对数据结构与生命周期的精准掌控。

















