Bitrise gradle-runner 因单元测试失败导致构建失败
结论
是的,这次构建失败的直接原因是 4 个单元测试没有通过。
日志已经给出了完整的因果链:
580 tests completed, 4 failed
> Task :app:testDebugUnitTest FAILED
Execution failed for task ':app:testDebugUnitTest'.
> There were failing tests.
BUILD FAILED
Gradle task failed, error: exit status 1
Gradle 执行 :app:testDebugUnitTest 时发现测试失败,任务因此返回非零退出码。Bitrise 的 gradle-runner 收到退出码 1,随后将整个 Step 和构建标记为失败。
所以,这不是 Bitrise 本身的故障,也不是 gradle-runner 的版本更新提示造成的。
变量解析错误和 Kotlin 版本警告并不是日志中记录的最终失败原因,但也不宜长期搁置。特别是 Kotlin 运行库版本不一致,可能带来编译或运行时问题。
首先修复失败的测试
失败用例集中在:
EnterCardDetailsViewModelSpec
其中包括:
it should validate ExpiryDate
it should emit continue button state
最后一个断言指向:
EnterCardDetailsViewModelSpec.kt:177
先在本地执行与 CI 相同的 Gradle 任务:
./gradlew :app:testDebugUnitTest
也可以只运行相关测试类,缩短排查时间:
./gradlew :app:testDebugUnitTest \
--tests "com.karmarama.edf.view.dashboard.payment.card.details.EnterCardDetailsViewModelSpec"
如果需要更多上下文,可以运行:
./gradlew :app:testDebugUnitTest --info --stacktrace
排查时重点关注:
EnterCardDetailsViewModelSpec.kt:177中的预期值和实际值。- ExpiryDate 测试使用的日期是否依赖当前年月。
- CI 和本地的时区、Locale 或系统时间是否不同。
- 异步任务、RxJava 调度器或协程是否在断言前执行完毕。
- 不同测试之间是否共享了可变状态。
- 测试依赖的环境变量、配置文件或密钥是否只存在于本地。
日期有效性测试很容易随着时间推移而失效。例如,测试中写死的有效期在编写时尚未过期,几个月后却可能变成无效数据。更稳妥的做法是使用固定时钟,或者通过明确注入的当前日期来生成测试数据,不要直接读取系统时间。
概念上可以采用类似的结构:
class EnterCardDetailsViewModel(
private val clock: Clock
) {
// 使用 clock 获取当前时间
}
测试时传入固定时间:
val fixedClock = Clock.fixed(
Instant.parse("2025-01-15T00:00:00Z"),
ZoneOffset.UTC
)
具体实现仍需结合项目使用的日期类型和 Android API 兼容要求调整。仅凭当前日志,还不能确定问题一定与时间有关。
查看测试报告
日志提供的报告路径是:
file:///Users/vagrant/git/app/build/reports/tests/testDebugUnitTest/index.html
这是 Bitrise 构建机器上的本地路径,通常无法直接从自己的电脑打开。可以选择以下方式处理:
- 在 Bitrise Workflow 中配置测试报告收集或部署步骤。
- 将
app/build/reports/tests/testDebugUnitTest/作为构建产物上传。 - 在本地重现问题,再打开对应路径下的
index.html。
测试报告通常包含 expected/actual、完整堆栈和各个失败用例的输出,比主构建日志更适合定位问题。
处理变量解析错误
日志中还有这些信息:
Failed to resolve variable '${project.groupId}'
Failed to resolve variable '${project.version}'
Failed to resolve variable '${version.asm}'
Failed to resolve variable '${version.junit}'
Failed to resolve variable '${version.mockito}'
这些错误很可能来自某个依赖的 Maven POM 元数据,或者是 Android Jetifier 处理依赖时遇到了未展开的 Maven 属性。由于构建之后仍然完成了依赖转换和编译,并进入单元测试阶段,所以从当前日志判断,它们没有直接导致这次构建中止。
可以先查明这些变量来自哪个依赖:
./gradlew :app:dependencies
针对测试运行时依赖,可以继续执行:
./gradlew :app:dependencyInsight \
--dependency junit \
--configuration debugUnitTestRuntimeClasspath
其他变量也可以分别检查对应依赖,例如:
./gradlew :app:dependencyInsight \
--dependency asm \
--configuration debugUnitTestRuntimeClasspath
找到来源后,可以采取以下措施:
- 升级生成异常 POM 的直接依赖。
- 将过旧或重复的测试库统一到兼容版本。
- 如果依赖来自已经废弃的仓库,改用仍在维护的发布源或版本。
- 检查仓库中发布的 POM 是否完整,不要直接在项目中随意定义同名 Gradle 变量。
不要仅仅为了消除日志,就在 gradle.properties 中添加 project.groupId、version.junit 等变量。它们很可能是依赖 POM 内部的 Maven 占位符,未必应该由当前项目提供。
统一 Kotlin 依赖版本
日志显示,运行时 classpath 中同时存在:
kotlin-reflect:1.2.60
kotlin-stdlib:1.3.21
kotlin-stdlib-common:1.3.21
这是实际存在的依赖一致性问题。虽然目前只产生了警告,但混用 Kotlin 1.2 和 1.3 运行库,可能导致二进制兼容、反射或运行时行为异常。
先查明 kotlin-reflect:1.2.60 是由哪个依赖引入的:
./gradlew :app:dependencyInsight \
--dependency kotlin-reflect \
--configuration debugUnitTestRuntimeClasspath
如果它由项目直接声明,应将其调整为项目正在使用的 Kotlin 版本:
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
}
如果旧版本由第三方测试框架传递引入,优先升级该框架。必要时,可以排除旧的传递依赖,再显式声明统一版本:
testImplementation("example:testing-library:version") {
exclude group: "org.jetbrains.kotlin", module: "kotlin-reflect"
}
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
这里的 example:testing-library:version 只是结构示例,必须替换为 dependencyInsight 实际查到的依赖,不能直接照搬。
是否需要升级 gradle-runner
日志中的:
Update available: 1.8.4 -> 1.8.5
只是 Step 的版本更新提示。这次失败已经明确出现在 Gradle 测试任务中,没有证据表明升级 gradle-runner 可以修复这 4 个失败的断言。
可以在后续维护中评估升级,但建议按照以下顺序处理:
- 修复或确认 4 个失败测试。
- 在本地执行与 Bitrise 相同的 Gradle 任务。
- 推送修改并重新触发构建。
- 清理变量解析错误和 Kotlin 版本冲突。
- 单独升级 Bitrise Step,并比较升级前后的构建行为。
不要通过忽略测试失败来让 PR 通过。Gradle 虽然可以配置 ignoreFailures,但这样会削弱 CI 拦截回归问题的能力。除非团队已经明确将这项任务定义为非阻断检查,否则失败测试仍应导致构建失败。
备注:内容仅供参考。
