用CircleCI构建一个Android应用程序并测试
CircleCI在Android开发者中很受欢迎,原因有几个:它可以快速上手,快速执行你的构建,具有很高的并行性,(无论是本地,跨平台或多平台),甚至支持直接从CircleCI与我们的Android机器图像运行Android模拟器。
本文将向您展示如何在CircleCI平台上为一个示例项目构建和测试Android应用程序。
要从这篇文章中获得最大的收获,你应该有
- 具有Android开发的工作知识
- 熟悉Android工具和测试框架
- 有能力使用Gradle构建系统
CircleCI的安卓版有什么可用?
我们已经为你提供了一些构建Android的工具。
- Docker镜像
- 安卓机器镜像
- 安卓球体
Docker镜像
Docker镜像是团队使用的主要执行工具。它在Docker容器中运行,所以启动起来非常快,并且包含了你可能需要的所有东西,可以开始构建Android项目。镜像还预装了Node.js、Android NDK和浏览器工具的变体。
机器图像
最大的变化是新的Android机器镜像。这是一个虚拟机镜像,所以比上面提到的Docker镜像需要更多的时间,但它包含了所有你可能需要的工具,以构建你的Android应用程序,包括SDK、Google Cloud的Firebase工具、Python、Node.JS和Fastlane。 然而,最大的变化是对嵌套虚拟化的支持,它允许你在这个机器镜像中运行Android模拟器。
安卓机器镜像支持嵌套虚拟化,这允许在CircleCI作业中运行x86模拟器。如果你从安卓平台的早期就开始开发,你可能记得模拟器非常慢。然后,x86模拟器开始支持英特尔的Hyper-V管理程序,这让模拟器直接使用主机的资源。像这样直接使用资源,使得模拟器能够更流畅、更快速地运行。
在很长一段时间里,支持仅限于我们的本地硬件。对CircleCI的有限支持拖慢了仿真器的速度,并使其难以运行UI测试。嵌套的虚拟化解决了这个问题,它将对快速仿真的支持添加到一个大规模的可重复的CI/CD环境中,使其所有的好处都可以得到。
Android Orb
最后是CircleCI的Android Orb,它提供了对作为执行器的图像以及安装SDK、运行仿真器、缓存、测试等方便的工作和命令的轻松访问。
介绍一下Android CI演示项目
我创建了一个演示项目,它是谷歌自己的安卓测试Codelab来源的一个分叉。Codelab是一个简单的TO-DO列表管理器应用程序,包含了单元测试和仪器测试的组合,有和没有UI。如果你在Android Studio中打开它,确保切换到Project
视图。项目视图可以让你查看我们要审查的.circleci/config.yml
文件。
该项目有一个单一的app
模块,有默认的debug
和release
变体。test
目录包含单元测试,而androidTest
目录包含仪器测试。完整的CircleCI配置包括在内,我将在本文中提到它。该配置位于.circleci/config.yml
。
构建和测试Android应用样本的CI
我们的CI过程必须做三件事。
- 在一个虚拟机内运行单元测试
- 在仿真器上运行仪表测试
- 构建应用程序的发布版本
我们将并排运行单元测试和仪器测试。如果这两种测试都成功了,我们就可以组装发布版的构建。我们还将添加一个条件,即只有在新的工作被提交到 "main "分支,并且在从23到30的每个Android版本的模拟器测试成功后,才能继续发布构建。当我们的构建满足这个条件时,我们将有坚实的保证,我们的应用程序将在许多不同的Android设备上运行。
注意: 正如我之前提到的,从这一点开始,一切都将参考CircleCI的配置文件,在.circleci/config.yml
。
使用CircleCI Android orb
创建CI管道的第一步是使用Android orb,它包含许多已经为你预写的步骤。
在定义了轨道后,脚本指定了这些工作。
unit-test
android-test
release-build
一个工作流程,test-and-build
,也包括在内。
version: 2.1
orbs:
android: "2.1.2"
jobs:
unit-test:
...
android-test:
...
release-build:
...
# We'll get to this later
workflows:
test-and-build:
...
# We'll get to this later
复制代码
运行单元测试
我们的unit-test
工作包含一个来自Android orb的执行器:android/android-docker
。如前所述,这是在Docker容器中运行的,启动速度非常快。
在steps
章节中,有几件事需要注意。首先,我们运行android/restore-gradle-cache
Gradle缓存帮助存储我们的依赖性和构建工件。在运行测试命令后,我们还运行android/save-gradle-cache
和android/save-build-cache
,以确保后续的构建通过使用缓存运行得更快。这些都来自Android orb,正如前缀android/
所示。
我们还可以通过将find-args
参数传递给restore-gradle-cache
,来修改被缓存的内容。
主要的构建步骤发生在android/run-tests
命令中,我们在一个test-command
参数中调用./gradlew testDebug
。这个命令为debug
构建变量运行所有单元测试。作为奖励,这个命令带有一个默认的重试值,以帮助你避免测试的失误。
jobs:
unit-test:
executor:
name: android/android-docker
tag: 2022.08.1
steps:
- checkout
- android/restore-gradle-cache
- android/run-tests:
test-command: ./gradlew testDebug
- android/save-gradle-cache
...
在模拟器上运行Android测试
我们的android-test
工作是使用新的安卓模拟器能力。该作业使用与build
作业相同的Android机器执行器。所有后续的作业都将使用相同的执行器,而且缓存恢复步骤也是一样的。
仿真器要求我们在机器镜像上运行,所以我们指定android/android-machine
作为执行器。为了提高构建时间,我们指定resource-class
为xlarge。你也可以在large
执行器上运行,但我发现,资源的缺乏会导致构建时间变慢。我建议你自己做一些实验,为你的项目找到合适的执行器大小。
下一步就更短了;我们只需要android/start-emulator-and-run-tests
命令。这个命令做的和它说的一样:它启动指定的仿真器并运行测试。我们再次传入test-command
作为参数(它使用android/run-tests
命令)。system-image
是完全合格的安卓模拟器系统镜像。我们目前将我们的安卓机器镜像捆绑到SDK的23版。你可以通过使用sdkmanager
工具来安装更多。
jobs:
android-test:
executor:
name: android/android-machine
resource-class: xlarge
steps:
- checkout
- android/start-emulator-and-run-tests:
test-command: ./gradlew connectedDebugAndroidTest
system-image: system-images;android-30;google_apis;x86
...
复制代码
你可以使用Android orb中指定的命令手动编码所有的模拟器设置步骤。create-avd
,start-emulator
,wait-for-emulator
, 和run-tests
。
储存测试结果
测试不仅仅是运行测试,因为我们可以从CircleCI仪表板内看到确切的失败内容,从而获得很多价值。为此,我们可以使用CircleCI平台内置的store_test_results
功能,这将显示我们通过(或失败)的构建。
单元测试和仪器测试的步骤略有不同,因为它们各自的Gradle任务将测试存储在不同的地方。 对于单元测试,测试将在app/build/test-results
,而对于仪器测试,它们将在app/build/outputs/androidTest-results
。
我们建议你创建一个新的Save test results
步骤,它使用find
工具并将所有测试结果文件复制到一个共同的位置 - 像test-results/junit
,然后从那里存储。还要确保在步骤中添加when: always
参数,这样无论上面的测试是成功还是失败,步骤都会运行。
存储单元测试
对于这个项目,我们想要基于XML的结果。调用store_artifacts
,使这些结果可以在CircleCI平台上解析。如果你愿意,你也可以存储HTML输出。
...
- android/run-tests:
test-command: ./gradlew testDebug
- run:
name: Save test results
command: |
mkdir -p ~/test-results/junit/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/junit/ \;
when: always
- store_test_results:
path: ~/test-results
- store_artifacts:
path: ~/test-results/junit
存储仪表测试
对于不同的测试类型,有不同的regex命令。其余存储测试结果的步骤与单元测试的例子相同。
- android/start-emulator-and-run-tests:
test-command: ./gradlew connectedDebugAndroidTest
system-image: << parameters.system-image >>
- run:
name: Save test results
command: |
mkdir -p ~/test-results/junit/
find . -type f -regex ".*/build/outputs/androidTest-results/.*xml" -exec cp {} ~/test-results/junit/ \;
when: always
- store_test_results:
path: ~/test-results
- store_artifacts:
path: ~/test-results/junit
进行发布构建并存储工件
对于这个项目,我们需要存储我们正在构建的应用程序。为了完成这部分工作,我们使用release-build
工作,它有两部分注意事项。一个run
步骤调用./gradlew assembleRelease
。然后,store_artifacts
指向构建的APK的位置。对于一个完整的CI/CD项目,你可以签署并上传该APK到一个测试版发布服务。你甚至可以用Fastlane在Play Store上创建一个版本。不过这两项活动都不在本文的讨论范围内。
jobs:
...
release-build:
executor:
name: android/android-machine
resource-class: xlarge
steps:
- checkout
- android/restore-gradle-cache
- android/restore-build-cache
- run:
name: Assemble release build
command: |
./gradlew assembleRelease
- store_artifacts:
path: app/build/outputs/apk/release
同时在多个Android版本上运行测试
安卓平台一直在发展,每个版本都有自己的错误和特殊性。我相信你已经遇到了其中的一些。避免不同版本的Android出现未定义行为的一个好方法是在尽可能多的版本上进行测试。CircleCI的作业矩阵功能使其更容易在多个版本上运行作业。
要使用作业矩阵,在你想在多个版本上测试的作业中添加一个参数。我们的android-test
作业使用了system-image
参数。我们还为该参数指定了一个默认值,所以你可以不使用该参数来运行作业。
要使用该参数,请将它放在成对的角括号内,指定你需要它的值 -<< >>
。
android-test:
parameters: # this is a parameter
system-image:
type: string
default: system-images;android-30;google_apis;x86
executor:
name: android/android-machine
resource-class: xlarge
steps:
- checkout
- android/start-emulator-and-run-tests:
test-command: ./gradlew connectedDebugAndroidTest
system-image: << parameters.system-image >> # parameter being used
要在工作流程中向参数传递数值,请使用我们正在调用的作业的matrix
参数。
workflows:
jobs:
...
- android-test:
matrix:
alias: android-test-all
parameters:
system-image:
- system-images;android-30;google_apis;x86
- system-images;android-29;google_apis;x86
- system-images;android-28;google_apis;x86
- system-images;android-27;google_apis;x86
- system-images;android-26;google_apis;x86
- system-images;android-26;google_apis;x86
- system-images;android-26;google_apis;x86
- system-images;android-26;google_apis;x86
name: android-test-<<matrix.system-image>>
通过缓存使每次运行更快
在构建我们的应用程序、安装依赖关系和运行测试的同时,我们往往也想利用缓存。这使得我们可以更少地进行重复的任务。
这可以让你跳过应用程序的预构建部分。
选择什么时候运行
在开发过程中,我们会创建许多提交,应该鼓励开发者尽可能多地将它们推送到远程仓库。不过每一个提交都会创建一个新的CI/CD构建,我们可能不需要在所有可能的设备上运行每一个测试,而只需要一个提交。另一方面,如果main
分支是我们的主要真相来源,我们希望在它上面运行尽可能多的测试,以确保它总是按计划工作。我们可能想只在main
,而不是其他任何分支上启动发布部署。
我们可以使用CircleCI的requires
和filters
段来定义适合我们流程的工作流程。我们的release-build
工作requires
,unit-test
工作。这意味着release-build
被放在队列中,直到所有的unit-test
工作都通过。然后才运行release-build
作业。
我们可以使用filters
来设置逻辑规则,只在特定的分支或git标签上运行一个特定的作业。例如,我们为我们的main
分支设置了一个过滤器,用一个完整的仿真器矩阵运行android-test
。在另一个例子中,release
构建只在main
上触发,并且只在其两个必要的作业成功运行后触发。
workflows:
test-and-build:
jobs:
- unit-test
- android-test: # Commits to any branch - skip matrix of devices
filters:
branches:
ignore: main
- android-test: # Commits to main branch only - run full matrix
matrix:
alias: android-test-all
parameters:
system-image:
- system-images;android-30;google_apis;x86
- system-images;android-29;google_apis;x86
- system-images;android-28;google_apis;x86
- system-images;android-27;google_apis;x86
- system-images;android-26;google_apis;x86
- system-images;android-25;google_apis;x86
- system-images;android-24;google_apis;x86
- system-images;android-23;google_apis;x86
name: android-test-<<matrix.system-image>>
filters:
branches:
only: main
- release-build:
requires:
- unit-test
- android-test-all
filters:
branches:
only: main # Commits to main branch
改进你的Android应用CI流程
我在本文中描述的步骤仅仅是一个开始。你可以用很多方法来改进这个流程。以下是一些想法。
构建一次,运行多次
如果你是一个有经验的Android开发者,你知道connectedAndroidTest
,总是会从一开始就运行整个构建过程。使用CircleCI,我们可以一次性构建整个应用程序和测试,将工件传递给下面的工作,并简单地在模拟器上运行测试。这个过程有可能为每个作业的运行节省数分钟的构建时间。
为了实现这一点,在每个模拟器的运行中添加三个命令行步骤:安装应用程序,安装仪表应用程序,并运行仪表测试。对于我们的应用程序,我们会输入。
adb install app/build/outputs/apk/debug/app-debug.apk
adb install app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
adb shell am instrument -w com.example.android.architecture.blueprints.reactive.test/androidx.test.runner.AndroidJUnitRunner
的确,这段代码会将输出发送到命令行。这并不是Gradle提供的整洁的测试输出。
在真实设备上运行
仿真器是一种工具,但真实世界的设备往往(可悲的是)大不相同。没有什么可以取代真实世界的设备测试。为此,你可以使用在云端提供真实设备的解决方案,如Sauce Labs或Firebase Test Lab。
部署给beta测试者,并发布应用程序
这个构建和测试示例项目只是更广泛的CI/CD故事的一个部分。你可以把你的CI/CD过程推进得更远。例如,你可以自动发布你的应用程序的新版本到Play Store。CircleCI有一些指南可以帮助你。
总结
在这篇文章中,我描述了用CircleCI构建一个Android应用程序,并对其进行测试。对于我们的示例项目,我们使用单元测试和Android平台模拟器的矩阵,使用新的CircleCI Android orb和机器图像。我们还谈到了如何存储和显示测试结果,以及如何协调工作流来运行测试不会给我们提供Gradle提供的整齐的测试输出,所有的输出都在一部分设备上。