持续集成(CI)中的单元测试:Jenkins/GitHub Actions配置自动测试流水线

说实话,很多团队写单元测试写得挺认真,但最后都烂在「没人跑」这三个字上。我见过不止一个项目,测试用例写了一两千条,结果大家只在提测前手动点一下——那跟没写有什么区别?

单元测试要想真正发挥作用,必须绑定到持续集成流水线里。每次提交代码,自动拉代码、自动编译、自动跑测试、自动出报告。谁破坏了测试,谁就得负责修。这才是正经玩法。

这一章,我就带你搭两条流水线:一条用 Jenkins,一条用 GitHub Actions。你根据团队情况选一个就行。

为什么非要把测试塞进 CI?

我刚开始带项目时,也犯过懒。觉得测试嘛,开发自己跑一遍就行了。结果呢?

  • 有人忘了跑测试就合代码
  • 有人跑了一半发现报错,直接跳过
  • 有人改了底层接口,上游测试全挂,但没人知道

直到一次上线前,集成测试炸了整整两天。从那以后,我立了个规矩:测试不通过,代码不许合。这个规矩,就是靠 CI 流水线来执行的。

核心原则:CI 中的单元测试应该是「门禁」——不通过,就拦住。不是「通知」——跑完了告诉你一声,你爱改不改。

方案一:Jenkins 流水线

Jenkins 是老牌 CI 工具,适合自建服务器的团队。我自己的经验是:别用自由风格项目,直接用 Pipeline(流水线)。Pipeline 用 Jenkinsfile 定义,代码化,能版本管理。

Jenkinsfile 示例

下面是一个 C 语言项目的典型 Jenkinsfile。它做了四件事:拉代码、编译、跑测试、生成报告。

pipeline {
    agent any

    tools {
        // 假设 Jenkins 上配了 GCC 工具链
        gcc 'GCC-11'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Build') {
            steps {
                sh 'mkdir -p build && cd build && cmake .. && make -j$(nproc)'
            }
        }

        stage('Unit Test') {
            steps {
                sh '''
                    cd build
                    ctest --output-on-failure -j4
                '''
            }
            post {
                always {
                    // 收集测试结果,Jenkins 会生成趋势图
                    junit 'build/test-results/*.xml'
                }
            }
        }

        stage('Coverage') {
            steps {
                sh '''
                    cd build
                    lcov --capture --directory . --output-file coverage.info
                    lcov --remove coverage.info '/usr/*' --output-file coverage.info
                    genhtml coverage.info --output-directory coverage_report
                '''
            }
            post {
                always {
                    // 发布覆盖率报告
                    publishHTML(target: [
                        reportName: 'Coverage Report',
                        reportDir: 'build/coverage_report',
                        reportFiles: 'index.html'
                    ])
                }
            }
        }
    }

    post {
        failure {
            // 测试失败时发邮件通知
            mail to: 'team@example.com',
                 subject: "Build Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
                 body: "请检查构建结果:${env.BUILD_URL}"
        }
    }
}
我的习惯:post 块里,我通常还会加一个 unstable 状态处理。有些测试是「非关键」的(比如性能测试),失败了不拦门禁,但发警告。Jenkins 里用 junit 步骤时,测试失败会让构建标记为 UNSTABLE,而不是 FAILURE

Jenkins 配置要点

配置项 说明 我的建议
触发器 什么时候触发构建 用 webhook + Poll SCM 双保险。webhook 实时触发,Poll SCM 兜底
测试报告 JUnit 格式的 XML 确保测试框架输出 JUnit 格式。CUnit、Check、Unity 都支持
超时设置 防止测试卡死 我一般设 30 分钟超时。超过就自动杀掉,标记为失败
并行执行 多机并行跑测试 如果测试用例多,用 parallel 分片跑。我分过 8 片,半小时缩到 5 分钟
踩坑提醒:我曾经在 Jenkins 上吃过一次大亏——测试环境里装了不同版本的 libc,导致测试结果跟开发环境不一致。后来我强制所有节点用 Docker 容器来跑测试,才彻底解决。建议你也用 Docker,Jenkins Pipeline 里直接 agent { docker 'gcc:11' } 就行。

方案二:GitHub Actions 流水线

如果你的代码托管在 GitHub 上,GitHub Actions 是最省事的选择。不用自己搭服务器,配置也简单。我个人觉得,小团队和开源项目用 Actions 更合适

GitHub Actions 工作流示例

在仓库根目录创建 .github/workflows/test.yml

name: C Unit Test CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Install dependencies
      run: |
        sudo apt-get update
        sudo apt-get install -y cmake gcc lcov

    - name: Configure and Build
      run: |
        mkdir -p build
        cd build
        cmake .. -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON
        make -j$(nproc)

    - name: Run unit tests
      run: |
        cd build
        ctest --output-on-failure -j4

    - name: Generate coverage report
      run: |
        cd build
        lcov --capture --directory . --output-file coverage.info
        lcov --remove coverage.info '/usr/*' --output-file coverage.info
        genhtml coverage.info --output-directory coverage_report

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        files: ./build/coverage.info
        fail_ci_if_error: true

    - name: Upload test results
      uses: actions/upload-artifact@v3
      with:
        name: test-results
        path: build/coverage_report/
小技巧:GitHub Actions 的 matrix 策略很好用。你可以一次跑多个编译器版本、多个平台。比如 matrix: { compiler: [gcc, clang], os: [ubuntu-latest, macos-latest] }。我常用这个来确保代码在不同环境下都能通过测试。

GitHub Actions 配置要点

配置项 说明 我的建议
触发条件 on push / on pull_request PR 触发是必须的。push 到 main 也建议触发,防止直接合代码
缓存 加速构建 actions/cache 缓存 build 目录和依赖。我见过从 5 分钟降到 40 秒的
测试矩阵 多配置并行 至少跑 Debug 和 Release 两种配置。Debug 开断言,Release 测性能
状态徽章 README 里显示构建状态 加上!开源项目靠这个吸引贡献者。闭源项目也方便团队快速了解主干状态

两种方案的对比与选择

你可能会问:到底选 Jenkins 还是 GitHub Actions?我个人的经验是这样的:

  • 选 Jenkins:你们有专门的运维团队,代码托管在自建 Git 服务器,或者需要高度定制化的流水线(比如要对接内部审批系统、要跑在特定硬件上)。
  • 选 GitHub Actions:代码在 GitHub 上,团队不大,不想折腾服务器。说白了,大部分中小团队选 Actions 就够了。

嗯,这里要注意一点:不要两个都上。我见过一个团队,Jenkins 和 Actions 同时跑,结果两边配置不一致,同一个测试一边过一边挂,排查起来特别痛苦。选一个,用到底。

CI 流水线的核心逻辑

下面这张图,是我自己总结的 CI 测试流水线核心流程。每次提交代码,都会走一遍这个循环:

CI 测试流水线核心流程 1. 代码提交 2. 自动拉取 & 编译 3. 运行单元测试 通过? ✅ 允许合并 ❌ 拒绝合并 每次提交自动触发,测试不通过代码不许合入

说白了,这个流程的核心就一句话:自动化、门禁化、可追溯。自动化是机器跑,门禁化是不通过就拦,可追溯是每次构建都有日志和报告。

常见问题与避坑

最后,分享几个我实际踩过的坑:

  • 测试环境不一致:开发机是 Ubuntu 22.04,CI 节点是 CentOS 7,链接了不同的库版本。解决方案——用 Docker 统一环境。
  • 测试用例太慢:一开始把所有测试都串行跑,一次构建要 40 分钟。后来分了 4 片并行,降到 12 分钟。再后来只跑「受影响」的测试,降到 3 分钟。
  • 误报太多:有些测试不稳定,偶尔过偶尔挂。我专门建了一个「不稳定测试」列表,先隔离排查,不拦门禁。
  • 报告没人看:测试报告生成了,但没人点开看。后来我在 PR 评论里自动贴摘要——通过率、覆盖率变化、失败用例列表。大家才真正开始关注。
最重要的教训:我曾经把 CI 配得太严格——代码风格检查、圈复杂度检查、测试覆盖率低于 80% 就失败。结果呢?开发们开始「应付」CI,写一堆没意义的测试来凑覆盖率。后来我放宽了门禁,只拦「真正有问题」的,反而效果更好。记住:CI 是帮助团队的工具,不是惩罚团队的手段

好了,这一章的内容就到这里。Jenkins 和 GitHub Actions 的配置示例都在上面了,你直接复制改改就能用。如果你们团队还在用手动跑测试,赶紧搭一条流水线——相信我,你会爱上那种「提交代码后自动告诉你结果」的感觉。


公众号:蓝海资料掘金营,微信deep3321