第二十三讲:测试报告生成——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集成其实就两步:

  1. 在构建脚本中运行测试,生成JUnit XML文件
  2. 在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%不能合并。这才把趋势拉回来。

知识体系总览

下面这张图总结了测试报告生成的核心流程:

测试报告生成与集成流程 测试框架执行 收集测试结果数据 生成JUnit XML文件 Jenkins解析与展示 关键输出 • 测试总数 • 通过/失败数 • 失败详情 • 执行耗时 • 错误堆栈 Jenkins趋势图: • 测试数量趋势 • 失败率趋势 • 耗时趋势 • 稳定性趋势

常见问题与避坑

我遇到过几个典型问题,分享出来大家少走弯路:

  • 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