第二十三讲:测试报告生成——JUnit XML格式输出与Jenkins集成
说实话,做了这么多年嵌入式开发,我见过太多团队在测试上栽跟头。不是没写测试,而是测试结果没人看。你想想看,每次跑完测试,输出一堆控制台日志,谁有耐心一条条翻?
我个人习惯是,从一开始就把测试报告这件事纳入架构设计。今天我们就聊聊怎么生成标准化的测试报告,再把它集成到Jenkins里,让测试趋势一目了然。
为什么是JUnit XML?
JUnit XML是Jenkins、GitLab CI、CircleCI等CI工具通用的测试报告格式。说白了,它就是一个约定好的XML结构,告诉CI工具:哪些测试通过了,哪些失败了,耗时多少。
我在项目中遇到过一件事:团队自己写了个测试框架,输出格式是自定义的JSON。结果换CI平台时,所有报告解析脚本都得重写。后来我强制要求所有测试框架都输出JUnit XML,再也没出过这种问题。
核心原则:测试报告格式要选行业标准,别自己发明轮子。
JUnit XML格式详解
一个典型的JUnit XML长这样:
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="MyTestSuite">
<testsuite name="test_math" tests="3" failures="1" errors="0" time="0.125">
<testcase name="test_add" classname="test_math" time="0.042">
</testcase>
<testcase name="test_sub" classname="test_math" time="0.038">
</testcase>
<testcase name="test_div" classname="test_math" time="0.045">
<failure message="division by zero" type="AssertionError">
Expected: 5.0, got: inf
</failure>
</testcase>
</testsuite>
</testsuites>
关键字段说明:
| 元素 | 属性 | 说明 |
|---|---|---|
| testsuites | name | 顶层容器,可包含多个testsuite |
| testsuite | name, tests, failures, errors, time | 一个测试套件,对应一个测试文件或模块 |
| testcase | name, classname, time | 单个测试用例 |
| failure | message, type | 测试失败时的详细信息 |
| error | message, type | 测试执行过程中抛出的异常 |
小技巧:classname属性通常填测试文件名或模块路径,这样Jenkins能按类名分组展示。
在C语言测试框架中生成JUnit XML
以Unity测试框架为例,它本身不直接输出JUnit XML。我一般会写一个简单的报告生成器。来看代码:
#include <stdio.h>
#include <string.h>
#include <time.h>
typedef struct {
char name[128];
char classname[128];
double time;
int passed;
char failure_msg[512];
} TestCase;
typedef struct {
char name[128];
TestCase cases[256];
int count;
double total_time;
int failures;
} TestSuite;
void junit_xml_write(TestSuite *suite, const char *filename) {
FILE *fp = fopen(filename, "w");
if (!fp) return;
fprintf(fp, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
fprintf(fp, "<testsuites name=\"AllTests\">\n");
fprintf(fp, " <testsuite name=\"%s\" tests=\"%d\" failures=\"%d\" errors=\"0\" time=\"%.3f\">\n",
suite->name, suite->count, suite->failures, suite->total_time);
for (int i = 0; i < suite->count; i++) {
TestCase *tc = &suite->cases[i];
fprintf(fp, " <testcase name=\"%s\" classname=\"%s\" time=\"%.3f\">\n",
tc->name, tc->classname, tc->time);
if (!tc->passed) {
fprintf(fp, " <failure message=\"%s\" type=\"AssertionError\">\n", tc->failure_msg);
fprintf(fp, " %s\n", tc->failure_msg);
fprintf(fp, " </failure>\n");
}
fprintf(fp, " </testcase>\n");
}
fprintf(fp, " </testsuite>\n");
fprintf(fp, "</testsuites>\n");
fclose(fp);
}
这段代码我用了好几年,基本没改过。核心思路就是:跑完每个测试用例后,把结果填进结构体,最后统一写XML。
注意:XML里的特殊字符(如 < > &)需要转义。上面的示例为了清晰没做转义,实际代码中一定要处理。
集成到Jenkins
Jenkins集成其实就两步:
- 在构建脚本中运行测试,生成JUnit XML文件
- 在Jenkins任务中添加"Publish JUnit test result report"后构建步骤
具体来说,在Jenkins的Pipeline脚本里这样写:
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'make'
}
}
stage('Test') {
steps {
sh './run_tests --output=test_results.xml'
}
}
stage('Report') {
steps {
junit 'test_results.xml'
}
}
}
}
嗯,这里要注意:junit指令的路径是相对于工作空间的。如果你把XML生成在子目录里,记得写对路径。
测试趋势图怎么看?
Jenkins集成后,会自动生成测试趋势图。我个人习惯关注三个指标:
- 测试总数变化——是不是在持续增加?如果突然减少,可能有人删了测试
- 失败率趋势——是不是在下降?如果突然上升,说明最近的提交引入了问题
- 耗时趋势——测试是不是越来越慢?如果某次提交后耗时翻倍,可能引入了性能问题
我曾经在一个项目里,发现测试总数连续三周没变。一查才知道,新来的同事不知道怎么写测试,一直在手动验证。后来我加了个CI门禁:测试覆盖率低于80%不能合并。这才把趋势拉回来。
知识体系总览
下面这张图总结了测试报告生成的核心流程:
常见问题与避坑
我遇到过几个典型问题,分享出来大家少走弯路:
- XML编码问题:测试用例名里如果有中文,记得XML声明里加encoding="UTF-8",否则Jenkins解析会乱码
- 时间精度:time属性单位是秒,但很多框架返回的是毫秒。我吃过这个亏,Jenkins上显示耗时0.001秒,其实是1毫秒
- 失败信息截断:failure元素里的内容如果太长,Jenkins展示时会截断。建议只保留关键信息,完整日志另存文件
- 多套件合并:如果有多个测试文件,每个生成一个XML,Jenkins的junit指令支持通配符:junit '**/test_results_*.xml'
我的习惯:每次提交代码前,本地跑一遍测试并生成XML。用脚本检查一下XML格式是否合法,避免CI上才暴露问题。
好了,关于测试报告生成和Jenkins集成,核心内容就这些。记住一句话:测试报告不是写给机器看的,是写给团队看的。格式标准、趋势清晰,才能真正推动质量改进。
公众号:蓝海资料掘金营,微信deep3321