在 CI 上运行 Android 仪器测试 - 从 Bitrise.io 到 GitHub Actions
快到 2020 年了,在 CI 上运行 Android Instrumented 测试仍然是一个挑战,尤其是对于开源项目和小型团队而言:
- 建立和管理内部设备农场成本高昂,需要长期投资和持续的基础设施支持——对于大多数团队来说这不是一个可行的选择。
- 对于大多数团队来说,基于云的设备农场(例如Firebase Test Lab)是一种更具成本效益的解决方案,因为它们负责管理基础架构,同时提供用于与现有 CI 管道集成的简单 API 。然而,对于需要进行大量测试的开源项目来说,这些服务提供的免费计划可能无法满足需求,而为此类服务付费通常也难以证明开源项目的合理性。
- 对于尚未准备好投资成熟的云端测试服务的小型团队来说,在 Docker 容器中的Android 模拟器上运行测试一直是最可行且最常见的方法。但如今,大多数云端持续集成 (CI) 服务仍然缺乏主机虚拟机的硬件加速支持,而这恰恰是阻碍其在现代 Android 模拟器(尤其是较新的 API 级别)上运行测试的首要因素。
我最近发布了FlowBinding,它包含 10 个库模块,包含超过160 个插桩测试。我希望能够在每个 PR 或合并到主分支时运行所有这些测试,而不必担心使用限制和配额之类的问题。Firebase Test Lab不在我的考虑范围内,因为免费版在虚拟设备上每天最多只能运行10 次测试,在实体设备上每天最多只能运行5 次测试。我需要一个能够满足以下条件的工具:
- 对于开源项目免费。
- 支持配置所使用的 Android 模拟器系统映像 - API 级别、目标:(默认或 google_apis)、arch/ABI(x86 或 x86_64)。
- 支持以无头模式运行模拟器(从模拟器 29.2.7 Canary开始,这相当于运行
emulator -no-window
)。 - 支持运行基于现代 x86 和 x86_64 的模拟器,并启用硬件加速(KVM)以获得更好的性能。
- 提供足够的 RAM 来运行Gradle和Emulator实例。
在这篇文章中,我将分享我为开源项目在 CI 上运行 Android 模拟器的最佳解决方案的历程。
硬件加速模拟器
在前面提到的这些要求中,在主机虚拟机上启用硬件加速支持是最难满足的要求。
旧版基于 ARM 的模拟器运行速度很慢。更重要的是,谷歌不再支持基于 ARM 的系统镜像,因为目前最新的 API 级别只提供 x86 和 x86_64 镜像。
现代 Intel Atom(x86 和 x86_64)模拟器旨在通过GPU 硬件加速提供更佳性能。然而,要启用硬件加速支持,需要安装特殊软件(Mac 和 Windows 上为 HAXM,Linux 上为 QEMU)。这给持续集成 (CI) 带来了挑战,因为要在 Docker 容器中运行硬件加速模拟器,主机虚拟机必须启用KVM,而由于基础设施限制,大多数基于云的持续集成 (CI) 提供商并不具备此功能。
这意味着目前我们无法在一些最流行的基于云的 CI 服务(如CircleCI和Travis)上运行硬件加速模拟器。
谷歌希望提供帮助
值得注意的是,谷歌最近一直在努力为在CI上运行模拟器提供更好的支持。
- 今年早些时候,Android 模拟器团队添加了在无头模式下运行模拟器引擎的支持,以减少在 CI 环境中运行模拟器的主机的一些隐式期望。此功能已在最近的版本
-no-window
中与该模式统一。 - 谷歌最近发布了一篇博文,重点介绍了他们正在开发的一些新工具,旨在提高模拟器在持续集成 (CI) 上的可部署性和可调试性。他们还开源了一系列用于在 Docker 容器中运行模拟器的脚本。
- 2019 年 Android 开发者峰会上有一场专门的会议,题为“持续集成 (CI) 环境中的模拟器” 。
- 此外,还有Project Nitrogen,这是 Google IO 2018 上推出的一项更大规模的测试,但今年尚未公开宣布其进展。
令人鼓舞的是,谷歌有兴趣并致力于不断改善在 CI 上运行模拟器的体验,但截至今天,KVM依赖性仍然是阻碍大多数用户利用这些新工具和改进的主要障碍。
Bitrise.io
bitrise.io是一个专注于移动端的 CI/CD 平台。它拥有自己的生态系统,包括在线工作流编辑器和丰富的工作流步骤库,这对于刚开始有基本移动端 CI 需求的新团队/项目来说可能很有吸引力。
但让我兴奋的是提供了运行 Android UI 测试(物理和虚拟设备)的步骤,以及主机环境中的KVM支持。
使用 Bitrise 步骤
Bitrise 提供了Android 构建 UI 测试和Android 虚拟设备测试步骤,这些步骤使用Firebase 测试实验室针对所选模块/构建变体运行测试,且设备使用时间/测试执行次数不受限制。但也存在一些限制:
- 每个步骤只能运行一个模块的一个构建变体。这意味着,如果您有多个包含插桩测试的库模块,则必须为每个模块手动配置一个Android 构建 UI 测试步骤或Android 虚拟设备测试步骤(或者,如果您愿意支付额外容器费用,可以设置并行工作流来并行运行这些步骤)。
- 除非存在应用 APK,否则无法在库模块中运行测试。解决方法是也
app:assembleDebug
为库模块运行测试。
您还可以使用AVD 管理器并等待 Android 模拟器步骤在 Bitrise 上本地启动模拟器,然后使用 Gradle 运行所有测试,但您无法完全控制模拟器配置和工作流程所需的特定 SDK 组件。
自定义工作流程
由于 Bitrise 的主机虚拟机已启用KVM,我们可以轻松设置自定义工作流,通过提供自定义 shell 脚本并使用常规 Gradle 命令运行测试来精确控制我们要下载/更新的 SDK 组件、我们如何配置和启动模拟器。
这是一个repo,展示了如何在 Bitrise 上设置这种自定义工作流程。
此时,我终于能够在每个 PR 上针对FlowBinding在 CI 上运行 Android 仪器测试了!
变得贪婪
在 CI 上免费运行无限量的插桩测试并非理所当然。但随着越来越多的模块和测试添加到FlowBinding中,Bitrise 工作流的构建时间也显著增加。
当我第一次集成 Bitrise 工作流程时,FlowBinding有2 个库模块和9 个仪器测试;整个 Bitrise 工作流程大约需要10 分钟(这包括运行自定义脚本来配置和启动模拟器,以及运行测试./gradlew connectedCheck
)。
当我准备发布FlowBinding的第一个版本时,已经有10 个库模块,总共有160 个仪器测试;整个 Bitrise 工作流程耗时超过30 分钟。
虽然该项目的模块和测试数量不太可能显著增加,但每个 PR 在 CI 上的构建时间超过 30 分钟仍然相当不便。
除了构建时间长之外,我还不太习惯使用在线编辑器配置工作流程,不得不依赖 Bitrise 提供的内置步骤。依赖平台和生态系统使得我们的流程在未来很难移植到可能更好的替代方案。
鉴于这些担忧和不满,我开始继续寻找在 CI 上运行仪器测试的更好体验。
GitHub Actions
GitHub Actions最近增加了对 CI/CD 的支持,并且对公共存储库免费。因此,GitHub 上的许多开源项目开始将其 CI/CD 工作流程迁移到 GitHub Actions,它与 GitHub 本身具有一流的集成,并由Azure提供强大的基础设施支持。
我首先尝试在Linux/Ubuntu虚拟机上的 Docker 容器中运行一个硬件加速的模拟器。不幸的是(或许也并不意外),主机上没有启用KVM 。
但是GitHub Actions也为公共仓库提供了免费的macOS虚拟机,这些 macOS 虚拟机也安装了HAXM(硬件加速执行管理器)。这很有希望,因为我们应该能够设置一个 GitHub 工作流,安装所需的 SDK 组件,并直接从主机启动硬件加速的模拟器,最后在模拟器上运行我们的测试!
macOS 上的自定义操作
然而,设置在macOS上运行的工作流并不像在Docker上运行那么简单,因为我们的基础环境不再是一个安装并配置好所有工具的 Docker 镜像,我们需要将脚本移植到 macOS 环境中。工作流需要执行以下操作:
- 安装/更新所需的Android SDK组件,包括构建工具、平台工具、平台(针对所需的 API 级别)、模拟器和系统映像(针对所需的 API 级别)。
- 使用各种配置选项(例如 API 级别、CPU 架构/ABI 和使用的设备配置文件)创建AVD的新实例。
- 从创建的 AVD 启动模拟器的新实例。
- 等待模拟器启动并准备使用。
- 最后执行 Gradle 命令来运行测试 - 例如
./gradlew connectedCheck
。
我们可以将此过程打包成自定义的 GitHub Action ,并为消费者提供一堆配置选项。
使用自定义操作(Android Emulator Runner )在API 29上运行仪表测试的工作流程可能如下所示:
jobs:
test:
runs-on: macOS-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
script: ./gradlew connectedCheck
结果
现在是时候使用FlowBinding运行自定义 GitHub Action 了,结果令人鼓舞!
整个 GitHub 工作流程大约需要20 分钟,而测试套件现在可以针对多个 API 级别并行运行(公共 repo 的最大并发 macOS 作业每个用户/组织为 5 个)。
这比之前采用Bitrise的解决方案提高了30%以上。
从纸面上看, GitHub Actions和Bitrise提供的构建机器规格相似——2 核 CPU / 7GB RAM。所以我不太确定性能提升的原因是什么。一种可能性是能够在基于 Docker 的环境中直接在主机虚拟机上运行所有内容,而无需虚拟化开销,但这取决于许多因素。
利用构建矩阵
我最喜欢的GitHub Actions功能之一是Build Matrix,它提供了一种在多个配置中运行同一项作业的简便方法。
例如,要针对 x86 和 x86_64 ABI 对 API 21、23、29 运行仪器测试,我们可以定义下方每个配置选项的值,matrix
并在操作的输入中引用这些值:
jobs:
test:
runs-on: macOS-latest
strategy:
matrix:
api-level: [21, 23, 29]
arch: [x86, x86_64]
steps:
- name: checkout
uses: actions/checkout@v2
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: ${{ matrix.arch }}
script: ./gradlew connectedCheck
工作流将使用全部 6 种配置运行该作业:
- API 21,x86
- API 21,x86_64
- API 23,x86
- API 23,x86_64
- API 29,x86
- API 29,x86_64
我们还可以过滤掉构建矩阵生成的某些配置。如果我们只想使用 (API 21, x86) 和 (API 29, x86_64) 运行作业,我们可以轻松地使用以下命令定义不需要的配置exclude
:
jobs:
test:
runs-on: macOS-latest
strategy:
matrix:
api-level: [21, 29]
arch: [x86, x86_64]
exclude:
- api-level: 21
arch: x86_64
- api-level: 29
arch: x86
steps:
- name: checkout
uses: actions/checkout@v2
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: ${{ matrix.arch }}
script: ./gradlew connectedCheck
这只会产生 2 个工作岗位:
- API 21,x86
- API 29,x86_64
您甚至可以疯狂地(请不要)并针对所有可用的配置组合运行测试,执行时将生成总共9 个API 级别(API 21 到 29)x 2 个目标(默认,google_apis)x 2 CPU / ABI(x86,x86_64)= 36 个作业。
🙃
有趣的事实- 在玩构建矩阵时我发现没有可用的系统映像android-27;google_apis;x86_64
。
更新- Google对缺失的系统映像的回应:
对于“android-27;google_apis;x86_64”,它缺失可能没有什么充分的理由。当时正值过渡期,我们刚刚迁移到 x86_64 内核,并采用 x86 32 位用户空间,我们需要处理由此带来的额外复杂性,因此一直没时间开发 x86_64 用户空间版本。不过,我们应该会在某个时候推出它的 x86 64 位用户空间版本,但目前它不在我们的待办事项清单上(抱歉)。
建立缓存?
我最喜欢CircirCI的一点是它原生支持缓存。由于我们能够直接缓存所有内容,从而通过CircleCI~/.gradle
获得极快的增量构建速度,所以我想知道是否可以做一些类似于我的Github 工作流程的事情,使其速度更快。
事实证明, GitHub 提供了这个用于缓存依赖项和构建输出的actions/cache 。
我们可以~/.gradle/caches
通过向作业添加以下步骤来缓存目录:
- uses: actions/cache@v1
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: ${{ runner.os }}-gradle-
不幸的是,这现在不起作用,因为缓存大小限制为200 MB,这对于 Gradle 来说不够,尽管他们计划在未来将限制增加到2GB 。
更新:每个缓存大小限制已增加到2GB,因此上述缓存配置现在可以工作了。
旅程永无止境
虽然自定义的 GitHub Action自迁移以来一直对FlowBinding运行良好,但我仍然希望找到一个更通用、更面向未来的解决方案,让所有内容都在 Docker 容器中运行,而不是直接在 macOS VM 上运行。对我来说,能够在未来轻松地尝试/迁移到其他 CI/CD 提供商非常重要,而使用直接在 macOS VM 上运行而不是在标准 Docker 环境上运行的工作流程可能会使实现这一点变得更加困难。
我最近偶然发现了一个不太知名的云 CI 提供商,它原生支持 KVM,并且对开源项目完全免费。在以后的文章中,我将探讨并分享我使用这款产品的体验,并将其与我目前的 CI 工作流程进行比较。
敬请关注!
鏂囩珷鏉ユ簮锛�https://dev.to/ychescale9/running-android-emulators-on-ci-from-bitrise-io-to-github-actions-3j76