CI/CD中的测试集成:让自动化测试真正跑起来
说实话,很多团队写测试写得挺认真,但一到CI/CD集成就翻车。我见过太多项目——本地测试全绿,一上CI就红成一片。为什么?环境不一样、依赖没装全、超时设置不对……说白了,CI/CD里的测试集成,不是把命令敲进去就完事了。
今天我就把GitHub Actions、Jenkins Pipeline、GitLab CI这三套主流方案怎么配测试,以及测试分片、并行执行、结果报告这些实战细节,一次性讲清楚。
核心观点:CI/CD里的测试集成,核心就三件事——跑得快、报得准、通知到人。
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里的测试集成,说白了就是三件事:
- 选对平台:GitHub Actions最省心,Jenkins最灵活,GitLab CI最平衡
- 跑得快:分片+并行,把测试时间压到可接受的范围
- 报得准:报告要详细,通知要分级,别让团队被无效信息淹没
嗯,这些配置看起来简单,但真正跑起来总会遇到各种意外。比如模拟器启动超时、Gradle缓存失效、测试设备离线……这些坑我基本都踩过。后面几章我会专门讲这些问题的排查方法,到时候再细聊。