基于 Jenkins 的 Android 自动化测试
目标
- 每次 push 自动触发测试
- 执行 Detekt 代码风格检测
- 执行 Java 单元测试
- 执行 Android 单元测试
- jenkins 构建结果页显示测试报告
- 构建结果通过飞书 Bot 通知到工作群
环境
- Mac OS (Apple M1)
- Jenkins (homebrew)
- self-hosted gitlab (ominus)
功能实现
创建新的构建任务,类型选择 Pipeline
设置 push 时触发构建
安装 jenkins 插件: gitlab
生成 webhook secrets (jenkins 默认不允许不安全的 webhooks 请求)
记录 webhook url 及 secret,回到对应的 gitlab 仓库,创建 webhook:
- 填写 URL 及 Secret token
- 勾选 Trigger 触发事件,如 Push events
- 勾选 Enable SSL verification
- 点击 Add webhook
执行 gradle 任务
Detekt 代码风格检测
1 | sh './gradlew clean detekt' |
Java 单元测试
1 | sh "./gradlew testDebugUnitTest" |
Android 单元测试
Android 单元测试需要依托 Android 设备,这里使用 Android 模拟器,方便后续维护。
启动 Android 模拟器:
1 | sh "emulator -avd $emulatorName" |
执行 Android 单元测试:
1 | sh "./gradlew connectedDebugAndroidTest" |
关闭 Android 模拟器:
1 | sh "adb emu kill" |
在实际工作环境中,上述命令会导致系统报错,解决方法参考 模拟器关闭时系统报错
记录测试报告
安装 jenkins 插件: publisher
Detekt 报告
detetk 建议配置 Merging reports 合并检测报告,合并后的报告文件路径:
1 | $projectDir/reports/detekt/merge.xml |
将合并后的报告展示在构建结果页。
1 | publishHTML (target : [allowMissing: false, |
Java 单元测试报告
Java 单元测试报告位置(moduleDir
替换为对应 module 的路径):
1 | $moduleDir/build/reports/tests/testDebugUnitTest/ |
将测试报告展示在构建结果页。
1 | publishHTML (target : [allowMissing: false, |
Android 单元测试报告
Android 单元测试报告位置(moduleDir
替换为对应 module 的路径):
1 | $moduleDir/build/reports/androidTests/connected/ |
将测试报告展示在构建结果页。
1 | publishHTML (target : [allowMissing: false, |
飞书通知
在飞书群组中,添加自定义机器人,获取 webhookUrl。开启方式参考飞书官方文档。
向 webhookUrl 发起网络请求,通知构建结果。
1 | def notifyFeishuBot(String webhookUrl, boolean success) { |
常见问题及解决方案
模拟器关闭时系统报错
具体原因暂时未知,猜测和模拟器缓存机制有关。
暂时的解决方案是直接 kill 模拟器进程,具体方式如下:
1 | // 获取模拟器进程 id |
执行 Android 测试时模拟器尚未启动
监听模拟器 sys.boot_completed
属性,从而判断模拟器是否启动完成。
1 | // 这里设置最多等待 120 秒,可根据电脑性能酌情增减 |
如果上述方法无效,也可以试试监听 init.svc.bootanim
属性,具体参考: stackoverflow
执行 Android 单元测试报错 INSTALL_FAILED_INSUFFICIENT_STORAGE
相关解释参考: stackoverflow
比较简单有效的解决方案是模拟器启动时添加 -wipe-data
参数,确保模拟器没有遗留数据。
单元测试报告样式错乱
Jenkins 默认禁止了结果页的 js/css 的加载,在 Manage Jenkins -> Script Console 输入下述命令,点击运行即可:
1 | System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' 'unsafe-inline' data:;") |
上述方法在 Jenkins 重启后即失效,需要重新配置。更建议的做法是直接修改 Jenkins 启动配置项。
具体参考: Managing the Content Security Policy on Jenkins
测试报告太多,如何区分哪一个测试失败
由于 Java/Android 的单元测试报告在每个 module 下独立生成,对于 module 较多的项目,可能会生成较多测试报告。在单元测试失败时,从构建结果页只能看到 Java/Android 单元测试失败,但具体是哪一个 module 下的需要一个一个报告点进去看,这个成本无疑是无法接受的。
两个解决思路:
- 读取每一份测试报告的用例 failures 数量,对于 failures 大于 1 的,在构建结果页特殊标记,如在测试报告标题上添加 (error) 标注。
- 对每个 module 下的每个测试任务,动态创建构建 stage,从而一眼区分出构建失败的具体任务。
这里我们对展示思路 1 的示例代码:
1 | // 读取 module 列表 |
上一步构建失败,导致整个构建任务中止
Jenkins pipeline Job 默认会在出错时中止整个构建任务,但对于我们的任务来说,哪怕 Detekt 检查未通过,我们也希望后续的单元测试能够执行。
Jenkins 提供了两个捕获错误的机制: try/catch
和 catchError
。两者的区别是,通过 try/catch
捕获的错误不会导致当前 stage 失败,从构建结果页来看,捕获到错误的 stage 构建状态是成功。而通过 catchError
捕获错误,则可以让我们自行决定当前 stage 是否要标记为失败。
回到我们的场景,我们希望每个 stage 的构建成功与否如实显示,只是不希望整个构建因为某一个 stage 的失败而中止,因此 catchError
更符合我们的需求。
1 | stage('detekt') { |
完整示例
源码: https://gist.github.com/yueban/c9d1ab1167eb842648e31511f5543e46