Android自动化测试与CI/CD实战

阅读时间: 23分钟
适合人群: Android开发者
你将收获: 掌握CI/CD流程,实现自动化部署


一个因手动打包引发的线上事故

2021年双11前夜,我们紧急修复了一个严重Bug。

晚上11点:开发完成,准备发版。

打包过程:

# 开发A:打包
./gradlew assembleRelease

# 开发B:签名
jarsigner -verbose -keystore release.jks app-release.apk

# 开发C:上传Google Play
# 手动上传...

# 开发D:更新版本号
# 手动修改build.gradle...

凌晨1点:上线完成,团队下班。

凌晨2点:用户投诉暴增。

问题诊断:

开发C上传了错误的APK文件
→ 上传的是旧版本!
→ Bug依然存在!
→ 双11当天崩溃率15%!

紧急回滚,但损失已造成:

损失统计:
- 销售额损失:100万
- 用户投诉:5000+条
- 应用评分:
- 团队通宵加班:10人

复盘:

根本原因:
1. 手动打包容易出错
2. 没有自动化流程
3. 没有版本控制
4. 没有测试验证

决定:
引入CI/CD自动化流程

实施CI/CD后:

自动化流程:
1. 代码提交 → 自动触发CI
2. 自动运行测试
3. 测试通过 → 自动打包
4. 自动签名
5. 自动上传到分发平台
6. 自动通知团队

效果:
- 发版时间:2小时 → 15分钟
- 出错概率:30% → 0%
- 团队效率:

这次事故让我明白:CI/CD是现代开发的标配。


什么是CI/CD?

CI (Continuous Integration)
持续集成:
- 自动编译
- 自动测试
- 及时发现问题

CD (Continuous Delivery/Deployment)
持续交付/部署:
- 自动打包
- 自动发布
- 快速交付

CI/CD工具选择

常用CI/CD平台

平台           特点                     适用场景
────────────────────────────────────────────────────
GitHub Actions 免费、集成好              开源项目
GitLab CI     功能强大、自托管          企业项目
Jenkins       老牌、插件多              复杂项目
CircleCI      云端、配置简单            中小项目
Travis CI     配置简单                  开源项目

GitHub Actions实战

1. 基础工作流

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

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

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout代码
        uses: actions/checkout@v3

      - name: 设置JDK
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: 授予Gradle执行权限
        run: chmod +x gradlew

      - name: 构建APK
        run: ./gradlew assembleDebug

      - name: 上传APK
        uses: actions/upload-artifact@v3
        with:
          name: app-debug
          path: app/build/outputs/apk/debug/app-debug.apk

2. 运行单元测试

name: Android Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: 设置JDK
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: 运行单元测试
        run: ./gradlew test

      - name: 上传测试报告
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: test-reports
          path: app/build/reports/tests/

      - name: 生成测试覆盖率
        run: ./gradlew jacocoTestReport

      - name: 上传覆盖率报告
        uses: codecov/codecov-action@v3
        with:
          files: ./app/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml

3. 自动发布

name: Release

on:
  push:
    tags:
      - 'v*'  # 推送tag时触发

jobs:
  release:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: 设置JDK
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: 解码Keystore
        run: |
          echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > release.jks

      - name: 构建Release APK
        run: ./gradlew assembleRelease
        env:
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

      - name: 创建Release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          draft: false
          prerelease: false

      - name: 上传Release APK
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: ./app/build/outputs/apk/release/app-release.apk
          asset_name: app-release.apk
          asset_content_type: application/vnd.android.package-archive

GitLab CI实战

完整Pipeline

# .gitlab-ci.yml
stages:
  - build
  - test
  - deploy

variables:
  ANDROID_COMPILE_SDK: "33"
  ANDROID_BUILD_TOOLS: "33.0.0"

before_script:
  - export GRADLE_USER_HOME=`pwd`/.gradle
  - chmod +x ./gradlew

cache:
  paths:
    - .gradle/wrapper
    - .gradle/caches

build_debug:
  stage: build
  script:
    - ./gradlew assembleDebug
  artifacts:
    paths:
      - app/build/outputs/apk/debug/app-debug.apk
    expire_in: 1 week

build_release:
  stage: build
  only:
    - tags
  script:
    - ./gradlew assembleRelease
  artifacts:
    paths:
      - app/build/outputs/apk/release/app-release.apk
    expire_in: 1 month

unit_test:
  stage: test
  script:
    - ./gradlew test
  artifacts:
    when: always
    reports:
      junit: app/build/test-results/test*/TEST-*.xml

lint_check:
  stage: test
  script:
    - ./gradlew lintDebug
  artifacts:
    paths:
      - app/build/reports/lint-results-debug.html
    when: always

deploy_internal:
  stage: deploy
  only:
    - develop
  script:
    - ./gradlew assembleDebug
    - |
      curl -F "file=@app/build/outputs/apk/debug/app-debug.apk" \
           -F "token=$FIREBASE_TOKEN" \
           https://upload.firebase.com/upload
  environment:
    name: internal
    url: https://appdistribution.firebase.com

deploy_production:
  stage: deploy
  only:
    - tags
  script:
    - ./gradlew assembleRelease
    - ./gradlew publishReleaseBundle
  environment:
    name: production
    url: https://play.google.com

自动化签名

1. Keystore配置

// build.gradle (Module: app)
android {
    signingConfigs {
        release {
            storeFile file(System.getenv("KEYSTORE_FILE") ?: "release.jks")
            storePassword System.getenv("KEYSTORE_PASSWORD")
            keyAlias System.getenv("KEY_ALIAS")
            keyPassword System.getenv("KEY_PASSWORD")
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

2. CI环境变量

# GitHub Secrets
KEYSTORE_BASE64: (base64编码的keystore文件)
KEYSTORE_PASSWORD: your_store_password
KEY_ALIAS: your_key_alias
KEY_PASSWORD: your_key_password

# 生成base64编码
base64 -i release.jks | pbcopy

自动化测试

1. UI测试(Espresso)

@RunWith(AndroidJUnit4::class)
class MainActivityTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    @Test
    fun testLoginFlow() {
        // 输入用户名
        onView(withId(R.id.username))
            .perform(typeText("testuser"), closeSoftKeyboard())

        // 输入密码
        onView(withId(R.id.password))
            .perform(typeText("password"), closeSoftKeyboard())

        // 点击登录
        onView(withId(R.id.login_button))
            .perform(click())

        // 验证跳转
        onView(withId(R.id.welcome_text))
            .check(matches(isDisplayed()))
    }
}

2. CI中运行UI测试

ui_test:
  stage: test
  script:
    - ./gradlew connectedDebugAndroidTest
  artifacts:
    paths:
      - app/build/reports/androidTests/connected/
    when: always

自动化部署

1. Firebase App Distribution

// build.gradle (Project)
buildscript {
    dependencies {
        classpath 'com.google.firebase:firebase-appdistribution-gradle:4.0.0'
    }
}

// build.gradle (Module: app)
plugins {
    id 'com.google.firebase.appdistribution'
}

firebaseAppDistribution {
    releaseNotesFile = "release-notes.txt"
    testers = "team@company.com, qa@company.com"
}
# CI配置
deploy_firebase:
  stage: deploy
  script:
    - ./gradlew assembleDebug appDistributionUploadDebug

2. Google Play Console

// build.gradle (Project)
buildscript {
    dependencies {
        classpath 'com.github.triplet.gradle:play-publisher:3.8.4'
    }
}

// build.gradle (Module: app)
plugins {
    id 'com.github.triplet.play'
}

play {
    serviceAccountCredentials.set(file("service-account.json"))
    track.set("internal")  // internal, alpha, beta, production
}
# CI配置
deploy_play_store:
  stage: deploy
  only:
    - tags
  script:
    - ./gradlew publishReleaseBundle

自动化通知

1. Slack通知

notify_slack:
  stage: deploy
  when: always
  script:
    - |
      curl -X POST -H 'Content-type: application/json' \
      --data "{\"text\":\"Build $CI_PIPELINE_ID finished with status: $CI_JOB_STATUS\"}" \
      $SLACK_WEBHOOK_URL

2. 钉钉通知

notify_dingtalk:
  stage: deploy
  when: always
  script:
    - |
      curl -X POST \
      "$DINGTALK_WEBHOOK" \
      -H 'Content-Type: application/json' \
      -d "{
        \"msgtype\": \"text\",
        \"text\": {
          \"content\": \"构建完成:$CI_PIPELINE_ID\"
        }
      }"

完整的CI/CD流程

否

是

否

是

否

是

内测

正式

代码提交

触发CI

编译代码

运行单元测试

测试通过?

通知失败

运行UI测试

测试通过?

代码检查

检查通过?

构建APK

签名

是否发布?

上传Firebase

上传Play Store

通知团队


最佳实践

1. 分支策略
   - main: 生产环境
   - develop: 开发环境
   - feature/*: 功能分支

2. 自动化测试
   - 单元测试覆盖率 > 60%
   - UI测试覆盖关键流程
   - 每次提交都运行测试

3. 代码审查
   - 强制Code Review
   - CI检查通过才能合并
   - 至少2人审查

4. 版本管理
   - 使用语义化版本(1.2.3)
   - Tag触发发布
   - 自动生成Changelog

5. 通知机制
   - 构建失败立即通知
   - 发布成功通知团队
   - 定期报告构建统计

总结

CI/CD是现代Android开发的基础设施。

核心价值:

1. 提升效率
   - 自动化构建
   - 快速反馈
   - 减少人工操作

2. 保证质量
   - 自动化测试
   - 代码检查
   - 及时发现问题

3. 降低风险
   - 标准化流程
   - 版本可追溯
   - 快速回滚

4. 团队协作
   - 统一流程
   - 透明可见
   - 提升士气

记住:

CI/CD不是奢侈品
而是现代开发的必需品

自动化 = 效率 + 质量

当你掌握了CI/CD,开发将更加高效和可靠。


下一篇预告:《Android开发最佳实践总结》


作者: Andy Gong
专注: Android自动化与DevOps

#Android #CICD #Automation #DevOps #GitHubActions #GitLabCI

更多推荐