Android 项目的 Instrumentation Tests 如何在 Github Actions 配置 CI/CD

2,200 阅读7分钟

在之前的《Kotlin Multiplatform 项目在 Github Actions 中配置 CI/CD》一文中比较详细地讨论了如何在 Github Actions 中配置 Kotlin Multuplatform 项目的 CI/CD,主要的步骤包括:环境设置、build、自动化测试、发布等等。

自动化测试是 CI/CD 中很重要的一环,由于之前我没找到如何在 CI/CD 的环境下启动 Android 模拟器,所以仅仅只运行了与 host 系统及架构对应的 Kotlin/Native unit tests,Android source set 内的代码的测试被忽略了。经过了一周的学习我找到了在 Github Actions 中启动 Android 模拟器并运行仪器测试(Instrumentation Tests)的方法。

基本用法

我们可以在 Github 的 marketplace 中的 Actions 下找到 Android Emulator Runner。启动运行 Android 模拟器就是用它。最基本的用法就是新建一个 step,yml 脚本代码如下:

jobs:
  test:
    runs-on: macos-latest
    steps:
      - name: checkout
        uses: actions/checkout@v3

      - name: run tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 29
          script: ./gradlew connectedCheck

核心代码就是指定 step 的 uses 为 reactivecircus/android-emulator-runner@v2 ,然后在 with 下配置一些诸如模拟器 api-level 以及执行 script(运行 instrumentation tests 的命令)等具体参数。

更多参数

在所有 with 下的参数中,只有 api-level 和 script 是必填的。除了它俩之外还有很多可选参数用于进行更详细的配置,比如:

- name: run tests
  uses: reactivecircus/android-emulator-runner@v2
  with:
    api-level: 31
    target: default
    arch: x86_64
    profile: Nexus 6
    script: ./gradlew connectedCheck

target 可以用于配置系统版本,关于系统版本是什么我们可以先看下图:

image.png

这是我们在 Android Studio 中创建 Android 模拟器时选择 Image 的页面,除了默认版之外,我们还可以选择 Google APIs 等版本,target 字段正式用于描述该选项,除了 default 之外也可以填 google_apis、playstore 等值。

arch 用于指定系统架构,目前 CI/CD 的各种 host 应该都是 x64 架构,所以我们选择 x86_64 即可。

在配置 api-level、target、arch 三个参数前,我们最好在 Android Studio 的模拟器创建页面查询一下我们选择的系统是否存在。例如目前 api 31 的系统的 x86_64 架构下只有 default 版本的系统,没有 Google APIs 的选项。api 32 和 33 的系统只有 arm64-v8a 架构,且只有 Google APIs 版本,没有 default 版本。所以配置前最好自己先查清楚自己想要配置的选项是否正确,以免在运行时找不到对应版本的系统而运行报错。

profile 字段用于指定运行的机器,上面的代码是官方示例,它配置的是 Nexus 6。大家都知道 Nexus 6 是一款老手机,所以我想要配置新机型,例如 Pixel 6,于是直接将 Nexus 6 替换成 Pixel 6。看起来合情合理但是运行会报找不到相关机型的错误。于是查询了 Android Emulator Runner 的文档后我发现,在填写机型前,我们需要在本地运行 Android SDK 中的一个命令:

avdmanager list device

avdmanager 在 Android SDK 的目录(sdk/cmdline-tools/latest/bin/)下,需要自行 cd 到该目录才能运行。运行该命令后展示了如下结果:

xxx bin % ./avdmanager list device
Available devices definitions:
id: 9 or "Nexus 6"
    Name: Nexus 6
    OEM : Google
---------
id: 27 or "pixel_6"
    Name: Pixel 6
    OEM : Google
---------

命令展示的结果很长,这里只摘出了两个 items。我们在 profile 字段中应该填的是 id 而不是 name,有趣的是 Nexus 6 的 name 和 id 都是 Nexus 6,因此填写 Nexus 6 可以运行。但是 Pixel 6 的 name 是 Pixel 6,但 id 则是 pixel_6,因此这里我们要给 profile 字段设置 pixel_6 才能启动 Pixel 6 设备。

NDK

如果你的 Android 工程中包含 ndk 代码,则可以用以下这些字段指定 ndk 版本以及 cmake 版本:

- name: run tests
  uses: reactivecircus/android-emulator-runner@v2
  with:
    api-level: 29
    ndk: 21.0.6113669
    cmake: 3.10.2.4988404
    script: ./gradlew connectedCheck

矩阵策略

有时候我们需要让我们的测试不仅仅只运行在一个 Android 版本的模拟器上,我们可能需要得到项目在多个不同的 api-level、target 上的测试结果,这时我们可以利用 Github Actions 的矩阵策略。矩阵策略用于配置一个 job,矩阵策略可以以类似配置环境变量的方式定义一些属性,并且给这些属性设置多个值(后面简称矩阵属性)。在 job 下的 step 中,可以以类似调用环境变量的语法将一些属性的值设置为先前定义的矩阵属性。在运行时被设置矩阵策略的 job 就会以矩阵值的总数相乘的数量并行运行,从而达到覆盖多种测试场景的目的。例如,如果我们希望自己的工程在多种 api-level 的模拟器上执行 instrumentation tests,我们可以这样配置:

build-mmkv-kotlin:
  runs-on: macos-latest
  strategy:
    matrix:
      api-level: [21, 23, 29]
      target: [default]

  steps:
    - name: Checkout the code
      uses: actions/checkout@v2

    - name: Instrumentation Tests
      uses: reactivecircus/android-emulator-runner@v2
      with:
        api-level: ${{ matrix.api-level }}
        target: ${{ matrix.target }}
        arch: x86
        profile: pixel_6
        script: ./gradlew connectedCheck --stacktrace

我们给 api-level 属性配置了三个值,target 虽然定义为了矩阵属性,但只有一个值。如果执行该 job,我们在 Github 上会看到这个 job 以不同的矩阵值并行运行了 3 次:

image.png

如果我们给 target 属性设置 2 个值,那么运行的 job 数量则会是 2 * 3 = 6 次。很多 Android SDK 中的 API 都会有 Android 系统版本限制,比如高于某版本则可用某 API 等。因此许多库在不同的 Android 版本上也许会有不同的具体实现。对于这类项目,通过设置矩阵策略,让我们的 instrumentation tests 在不同版本的 Android 模拟器上运行是一种非常必要的做法。

上传测试报告

我们可以使用以下脚本上传测试报告:

- name: Upload Reports
  uses: actions/upload-artifact@v2
  with:
    name: Test-Reports
    path: app/build/reports
  if: always()

把上面这个 step 添加到 tests 的 step 后面即可。它和 instrumentation tests 没有关系,任意自动化测试的结果都可以上传。配置在于选择正确的测试报告路径并配置到 path 属性。条件语句 if: always() 表示总是上传测试报告,如果你只想在测试失败的时候上传测试报告则可以改成 if: failure()。在 workflow 执行完毕后,我们在 Github Actions 下点击这个具体执行完毕的 action,即可在 Artifacts 这个板块内看到测试报告并下载,如下图:

image.png

总结

如果你的 Android 项目比较简单,且对 Android SDK 的依赖并不多的话,运行普通的 unit tests 即可。如果你的 Android 项目对系统 SDK 依赖较多,或者你希望在非 Mock 的环境下看到更真实的测试结果,又或者是你的项目中有 NDK 代码或直接依赖 so 库,instrumentation tests 就对你来说非常必要了。

对于 Kotlin Multiplatform 项目来说,Android 是需要被支持的重点平台,并且它与其他 targets 的环境差异较大,因此 Android instrumentation tests 也是必要的配置项。

对于我们的两个开源项目:MMKV-Kotlin 和 SQLlin,我已经添加了 instrumentation tests,不过由于 Kotlin Multiplatform 复杂的构建、测试、发布流程(多平台编译构建产物、多 host build、多次发布),目前我还没有配置矩阵策略。对于 SQLlin 来说,以 Android O 为分界线,它在高低两种版本的 Android 上有不同的实现,矩阵策略在未来是必要的,等后续有时间再整体优化一下 SQLlin 的 CI/CD 策略。

参考链接