如何在JUnit XML结果中合并Spock @Unroll测试用例
我编写了带@Unroll注解的Spock参数化测试:
@Unroll def 'A session uses the TLS protocol #enabledProtocolsOfSocket #protocolOfSession'() { given: // ... expect: // ... where: enabledProtocolsOfSocket | protocolOfSession ["TLSv1.3"] | "TLSv1.3" ["TLSv1.2", "TLSv1.3"] | "TLSv1.3" ["TLSv1.2"] | "TLSv1.2" ["TLSv1.2", "TLSv1.1"] | "TLSv1.2" ["TLSv1.1", "TLSv1.2"] | "TLSv1.2" ["TLSv1.1"] | "NONE" ["TLSv1"] | "NONE" ["TLSv1", "TLSv1.1"] | "NONE" ["TLSv1.1", "TLSv1"] | "NONE" }
该测试生成的JUnit结果XML如下:
<?xml version="1.0" encoding="UTF-8"?> <testsuite name="com.acme.TlsProtocolSpec" tests="9" skipped="0" failures="0" errors="0" timestamp="2024-07-19T17:23:47" hostname="acme-build5eb650" time="0.403"> <properties/> <testcase name="A session uses the TLS protocol [TLSv1.3] TLSv1.3" classname="com.acme.TlsProtocolSpec" time="0.081"/> <testcase name="A session uses the TLS protocol [TLSv1.2, TLSv1.3] TLSv1.3" classname="com.acme.TlsProtocolSpec" time="0.037"/> <testcase name="A session uses the TLS protocol [TLSv1.2] TLSv1.2" classname="com.acme.TlsProtocolSpec" time="0.084"/> <testcase name="A session uses the TLS protocol [TLSv1.2, TLSv1.1] TLSv1.2" classname="com.acme.TlsProtocolSpec" time="0.07"/> <testcase name="A session uses the TLS protocol [TLSv1.1, TLSv1.2] TLSv1.2" classname="com.acme.TlsProtocolSpec" time="0.071"/> <testcase name="A session uses the TLS protocol [TLSv1.1] NONE" classname="com.acme.TlsProtocolSpec" time="0.014"/> <testcase name="A session uses the TLS protocol [TLSv1] NONE" classname="com.acme.TlsProtocolSpec" time="0.013"/> <testcase name="A session uses the TLS protocol [TLSv1, TLSv1.1] NONE" classname="com.acme.TlsProtocolSpec" time="0.013"/> <testcase name="A session uses the TLS protocol [TLSv1.1, TLSv1] NONE" classname="com.acme.TlsProtocolSpec" time="0.013"/> <system-out><![CDATA[]]></system-out> <system-err><![CDATA[]]></system-err> </testsuite>
问题在于:后续处理结果文件时,无法将这些结果映射到原测试方法——既没有保留带占位符的原测试名称A session uses the TLS protocol #enabledProtocolsOfSocket #protocolOfSession,也没有能关联参数化实例与原方法的结构。
我希望能将所有参数化测试实例归到原测试方法的分组下,类似xUnit的<collection>元素结构:
<collection total="9" passed="9" failed="0" skipped="0" name="Test collection for Com.Acme.SomeClass.Tests.SomeTestName" time="0.484"> <test name="Com.Acme.SomeClass.Tests.SomeTestName(someParam: "10")" ...> </test> <test name="Com.Acme.SomeClass.Tests.SomeTestName(someParam: "11")" ...> </test> ... </collection>
只要能保留原测试方法与所有参数化运行的关联(即一个实例失败则整个测试方法标记为失败),无论是通过JUnit输出配置还是Spock特性实现都可以。
更新说明:
当前使用Gradle 8.5(Groovy 3.0.17,Spock 2.4-M4-groovy-4.0),测试配置如下:
test { reports { junitXml.required = true html.required = true } useJUnitPlatform() }
已尝试mergeReruns设置(仅适用于失败重试测试)和Test Report Aggregation Plugin(按类型聚合而非测试用例),均无法解决问题。
解决方案
1. 调整@Unroll命名模板,保留原方法标识
修改测试名称模板,在参数化实例名称中加入原方法的固定前缀,同时让后续处理能通过前缀识别归属:
@Unroll def 'A session uses the TLS protocol [enabledProtocols: #enabledProtocolsOfSocket, expected: #protocolOfSession]'() { // 测试逻辑不变 }
这种方式能让每个参数化实例的名称都包含原方法的核心描述,方便后续脚本或工具按前缀分组关联。
2. 配置Gradle+JUnit平台生成层级化报告
修改Gradle测试配置,启用JUnit平台的层级报告特性,让参数化测试归属于原方法节点下:
test { reports { junitXml.required = true html.required = true } useJUnitPlatform { // 启用标准显示名生成器,保留原方法结构 configurationParameter 'junit.jupiter.displayname.generator.default', 'org.junit.jupiter.api.DisplayNameGenerator$Standard' // 开启详细报告,保留参数化测试的层级关系 configurationParameter 'junit.platform.reporting.show-details', 'true' } // Spock专属配置:按原测试方法分组参数化实例 jvmArgs '-Dspock.reporting.unroll.group-by-feature=true' }
配置后生成的JUnit XML会将同一原方法的参数化实例归为逻辑分组,classname字段会包含原方法的完整标识,便于关联映射。
3. 移除@Unroll,合并参数化结果
如果不需要单独查看每个参数实例的执行详情,可以直接移除@Unroll注解。此时Spock会将整个参数化测试作为一个测试用例执行:
- 所有参数实例的结果合并到原方法名下
- 只要有一个实例失败,整个测试方法直接标记为失败
- JUnit XML中只会生成一个对应原方法的
testcase节点,包含整体执行结果
4. 自定义XML后处理脚本
如果上述配置无法满足需求,可以编写Gradle任务手动修改生成的JUnit XML:
- 遍历测试输出目录下的XML文件
- 按测试名称的共性前缀(原方法描述)对
testcase元素分组 - 重新生成包含自定义
<collection>节点的XML结构,将同组实例归入对应集合
内容的提问来源于stack exchange,提问作者crusy

