CI/CD中的测试集成:让自动化测试真正跑起来

说实话,很多团队写测试写得挺认真,但一到CI/CD集成就翻车。我见过太多项目——本地测试全绿,一上CI就红成一片。为什么?环境不一样、依赖没装全、超时设置不对……说白了,CI/CD里的测试集成,不是把命令敲进去就完事了。

今天我就把GitHub Actions、Jenkins Pipeline、GitLab CI这三套主流方案怎么配测试,以及测试分片、并行执行、结果报告这些实战细节,一次性讲清楚。

核心观点:CI/CD里的测试集成,核心就三件事——跑得快、报得准、通知到人。

CI/CD测试集成知识体系 测试集成核心 CI平台配置 测试执行策略 结果与通知 GitHub Actions Jenkins Pipeline GitLab CI 测试分片 并行执行 测试报告 通知机制

GitHub Actions配置:最省心的选择

我个人最喜欢GitHub Actions。为什么?因为它跟GitHub仓库天然集成,配置起来最轻量。你只需要在仓库里建一个.github/workflows/目录,放个YAML文件就行。

基础配置模板

先看一个最基础的Android测试工作流。嗯,这里要注意——gradlew的权限问题我踩过坑,记得加上chmod

# .github/workflows/android-test.yml
name: Android CI

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
      
      - name: Run unit tests
        run: ./gradlew testDebugUnitTest
      
      - name: Run UI tests
        run: ./gradlew connectedDebugAndroidTest
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: |
            app/build/reports/tests/
            app/build/reports/androidTests/

我的经验:if: always()这个条件很重要。默认情况下,如果前面的步骤失败了,后续步骤不会执行。但测试报告你肯定想保留,哪怕测试挂了。我曾经因为没加这个,调试了整整一个下午……

矩阵测试与分片

如果你的项目比较大,测试跑半小时那种,就得考虑分片了。GitHub Actions支持矩阵策略,可以按模块或按测试类拆分。

jobs:
  test:
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    
    steps:
      - name: Run tests with sharding
        run: |
          ./gradlew testDebugUnitTest \
            -Pandroid.testInstrumentationRunnerArguments.numShards=4 \
            -Pandroid.testInstrumentationRunnerArguments.shardIndex=${{ matrix.shard }}

说白了,就是把测试用例分成4份,每份跑在不同的worker上。我有个项目原来跑45分钟,分片后12分钟搞定——这感觉,你懂的。

Jenkins Pipeline:老牌选手的优雅

Jenkins虽然配置起来比GitHub Actions繁琐,但它的Pipeline as Code理念非常强大。尤其是复杂的多阶段构建,Jenkins的灵活性无可替代。

Declarative Pipeline示例

// Jenkinsfile
pipeline {
    agent any
    
    tools {
        gradle 'Gradle 8.0'
        jdk 'JDK17'
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Unit Tests') {
            steps {
                sh './gradlew testDebugUnitTest'
            }
            post {
                always {
                    junit '**/build/test-results/**/*.xml'
                }
            }
        }
        
        stage('UI Tests') {
            steps {
                sh './gradlew connectedDebugAndroidTest'
            }
            post {
                always {
                    junit '**/build/outputs/androidTest-results/**/*.xml'
                }
            }
        }
        
        stage('Test Report') {
            steps {
                publishHTML([
                    reportDir: 'app/build/reports/tests/testDebugUnitTest',
                    reportFiles: 'index.html',
                    reportName: 'Unit Test Report'
                ])
            }
        }
    }
    
    post {
        failure {
            emailext(
                subject: "构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
                body: "请查看构建结果: ${env.BUILD_URL}",
                to: 'team@example.com'
            )
        }
    }
}

避坑指南:Jenkins的junit步骤默认只保留最近10次的结果。如果你需要长期保留测试趋势,记得配置buildDiscarder。我曾经因为没配这个,被QA追着要上周的测试报告……

并行执行与分片

Jenkins的并行执行可以用parallel块来实现。我个人习惯把单元测试和UI测试并行跑,能省不少时间。

stage('Parallel Tests') {
    parallel {
        stage('Unit Tests') {
            steps {
                sh './gradlew testDebugUnitTest'
            }
        }
        stage('UI Tests - Shard 1') {
            steps {
                sh './gradlew connectedDebugAndroidTest -PshardIndex=0 -PnumShards=3'
            }
        }
        stage('UI Tests - Shard 2') {
            steps {
                sh './gradlew connectedDebugAndroidTest -PshardIndex=1 -PnumShards=3'
            }
        }
        stage('UI Tests - Shard 3') {
            steps {
                sh './gradlew connectedDebugAndroidTest -PshardIndex=2 -PnumShards=3'
            }
        }
    }
}

GitLab CI配置:自托管的首选

GitLab CI的配置风格介于GitHub Actions和Jenkins之间。它的.gitlab-ci.yml文件结构清晰,而且自带的Runner管理非常方便。

基础配置

# .gitlab-ci.yml
image: openjdk:17-jdk

variables:
  GRADLE_OPTS: "-Dorg.gradle.daemon=false"
  ANDROID_COMPILE_SDK: "34"
  ANDROID_BUILD_TOOLS: "34.0.0"

before_script:
  - apt-get update && apt-get install -y wget unzip
  - wget -q https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip
  - unzip commandlinetools-linux-*.zip -d /opt/android-sdk
  - export ANDROID_HOME=/opt/android-sdk
  - export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin
  - yes | sdkmanager --sdk_root=$ANDROID_HOME "platforms;android-$ANDROID_COMPILE_SDK"
  - chmod +x gradlew

stages:
  - test
  - report

unit-test:
  stage: test
  script:
    - ./gradlew testDebugUnitTest
  artifacts:
    paths:
      - app/build/reports/tests/
    expire_in: 1 week

ui-test:
  stage: test
  script:
    - ./gradlew connectedDebugAndroidTest
  artifacts:
    paths:
      - app/build/reports/androidTests/
    expire_in: 1 week

test-report:
  stage: report
  script:
    - echo "Generating test report..."
  dependencies:
    - unit-test
    - ui-test
  artifacts:
    paths:
      - app/build/reports/

我的建议:GitLab CI的artifacts一定要设置expire_in。不然跑几个月下来,你的服务器硬盘就满了。我见过一个团队,artifact攒了200多G,清理的时候差点把生产数据也删了……

测试分片与并行执行:让CI快起来

测试分片的核心思想很简单——把测试用例拆成多份,同时跑。但怎么拆,这里头有讲究。

分片策略对比

策略 优点 缺点 适用场景
按模块分片 实现简单,模块间无依赖 模块大小不均,负载不均衡 多模块项目
按测试类分片 粒度细,负载均衡好 需要额外工具支持 大型单体项目
按测试方法分片 最均衡 实现复杂,开销大 测试用例极多
时间预估分片 基于历史数据,最智能 需要历史数据积累 成熟项目

我个人比较推荐按测试类分片。为什么?因为实现起来不复杂,而且效果已经很好了。你想想看,如果每个测试类跑的时间差不多,那按类分片基本就是最优解。

Gradle中的分片配置

// build.gradle.kts (模块级)
android {
    defaultConfig {
        testInstrumentationRunnerArguments["numShards"] = System.getProperty("numShards") ?: "1"
        testInstrumentationRunnerArguments["shardIndex"] = System.getProperty("shardIndex") ?: "0"
    }
}

// 或者用命令行参数
// ./gradlew connectedDebugAndroidTest -PnumShards=4 -PshardIndex=0

注意:分片不是越多越好。每个分片都有启动开销,分得太细反而更慢。我一般建议分片数不超过CI可用CPU核心数的2倍。比如4核的机器,分8片就差不多了。

测试结果报告与通知

测试跑完了,结果得让人知道。这里我分享几个常用的方案。

报告格式与工具

  • JUnit XML格式:几乎所有CI工具都支持,通用性最强
  • HTML报告:Gradle自带的testReport任务就能生成,直观好看
  • Allure Framework:功能最强大,支持历史趋势、分类统计
  • 自定义JSON:适合对接内部系统

通知渠道配置

通知这块,我建议分级处理:

  • 构建失败:立即通知到人(邮件+即时通讯)
  • 测试失败:通知到团队频道
  • 测试通过但有警告:汇总到日报
  • 全部通过:不通知(没人想看"一切正常"的消息)
// GitHub Actions 中的通知示例
- name: Notify on failure
  if: failure()
  uses: slackapi/slack-github-action@v1.24.0
  with:
    payload: |
      {
        "text": "❌ 构建失败: ${{ github.repository }}/${{ github.run_id }}"
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

我的习惯:测试报告里一定要包含失败用例的堆栈信息截图。光说"测试失败"没用,得让人一眼看出哪里挂了。我在Allure里配了截图自动上传,每次失败都能看到当时的界面状态——调试效率翻倍。

总结一下

CI/CD里的测试集成,说白了就是三件事:

  1. 选对平台:GitHub Actions最省心,Jenkins最灵活,GitLab CI最平衡
  2. 跑得快:分片+并行,把测试时间压到可接受的范围
  3. 报得准:报告要详细,通知要分级,别让团队被无效信息淹没

嗯,这些配置看起来简单,但真正跑起来总会遇到各种意外。比如模拟器启动超时、Gradle缓存失效、测试设备离线……这些坑我基本都踩过。后面几章我会专门讲这些问题的排查方法,到时候再细聊。

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