用途
该技能用于在 Android 项目中落地测试策略,围绕三个目标:测试必须确定性(同一输入得到同一结果)、结构化(任何读者都能定位 Given/When/Then 三段)、可替换(一次绑定替换即可把生产依赖图改为假实现)。它同时整理 Google 的策略指导,并标注每条规则实际出自哪个页面,以及把这些规则接入构建的 Hilt 机制。
适用场景:用户询问“如何组织我的 Android 测试”、Given-When-Then 与 Arrange-Act-Assert 的取舍、测试方法命名;询问 @HiltAndroidTest、HiltAndroidRule、@TestInstallIn、@UninstallModules、@BindValue 或规则顺序;报告“本地通过但 CI 失败”(通常是确定性问题);新建模块并需要策略文档或约定清单;提到用 RuleChain.outerRule 组合 HiltAndroidRule 与 ActivityScenarioRule 或 composeTestRule。
不适用场景:选择测试范围(小/中/大)应使用测试金字塔相关技能;选择要覆盖的行为、选择假实现还是 mock、接线 source set、调试单个不稳定的 Compose 动画测试,均属于其他技能。
前提
熟悉小/中/大金字塔框架;存在依赖注入的测试目标(Hilt、手动构造注入或其他 DI 库);Hilt 部分需要 androidTestImplementation 上有 dagger.hilt.android.testing.HiltAndroidRule 与 HiltAndroidTest;确定性部分需要 kotlinx-coroutines-test、Robolectric(如做宿主端测试),以及可对 System.currentTimeMillis() / Clock.systemDefaultZone() 打桩。
来源对照(哪一页写了什么)
策略概念分散在多个页面,标明来源很重要,因为评审会引用技能。质性金字塔(“多小、少大”)、五级 Unit/Component/Feature/Application/RC 表、各层网络访问均出自 /training/testing/fundamentals/strategies;确定性/不稳定处理出自 /training/testing/instrumented-tests/stability(不是 /strategies);封闭性测试的定义在 /training/testing/fundamentals/test-doubles 中顺带提及;Given-When-Then 注释风格在任何页面都未被命名,只在代码示例中使用;Arrange-Act-Assert 在任何页面均未被命名;测试命名只在 /training/testing/instrumented-tests 有反引号的注意事项;@HiltAndroidTest、HiltAndroidRule、@TestInstallIn、@UninstallModules、@BindValue 出自 /training/dependency-injection/hilt-testing。不得把确定性指导归到 /strategies;不得声称 Google“推荐 Given-When-Then”。
确定性
四类非确定性来源占主导:时间(System.currentTimeMillis()、Clock.systemDefaultZone()、Instant.now(),应注入 Clock 或使用 kotlinx-coroutines-test 虚拟时间);并发/协程(直接用 Dispatchers.IO、runBlocking、真实 delay,应使用 runTest、注入 CoroutineDispatcher,通过 Dispatchers.setMain(dispatcher) 替换主调度器);动画/帧时序(Compose 的 MainTestClock、View 动画,可通过 Gradle 的 testOptions.animationsDisabled = true 或 ADB 的 Settings.Global.WINDOW_ANIMATION_SCALE = 0 关闭;Compose 可设置 mainClock.autoAdvance = false);外部状态(网络、真实数据库、Random()、UUID.randomUUID(),应注入种子、使用假实现、每个仪器测试前执行 pm clear)。
具体清单:注入 Clock(或等价时间源),测试用 Clock.fixed(Instant.parse(...), ZoneId.of("UTC"));通过 DispatcherProvider 接口注入调度器,或在 @Before 中用 Dispatchers.setMain(StandardTestDispatcher()) 替换;为 Random 设置种子(Random(42))或封装成可替换的 RandomProvider;在 build.gradle.kts 中设置 testOptions.animationsDisabled = true;CI 在每个仪器测试前执行 pm clear <pkg>(Test Orchestrator 会自动做,否则加 @Before)。
封闭性与集成
/strategies 页面并未把“封闭(hermetic)”作为顶层术语正式化。最接近的定义在 /test-doubles:“封闭测试避免所有外部依赖,例如从互联网获取数据。”操作性规则在 /strategies:Unit 与 Component 层无网络访问;Feature 层“支持 mock 网络访问”;Application 与 Release Candidate 层不适用。讨论封闭性时应同时引用两页。
Given-When-Then 与 Arrange-Act-Assert
两种模式均未在四个主页面被命名,但在 Google 代码示例中可见。可准确表述为“Google 官方示例一致使用 Given-When-Then 注释块”,不得表述为“Google 推荐 Given-When-Then”。两者形状相同、词汇不同:Given(Arrange)搭建被测系统与依赖;When(Act)调用被测行为;Then(Assert)断言结果状态或交互。每个仓库选一种词汇(skydoves 偏好 Given-When-Then),用作行内注释块而非方法名。
测试命名
四个主页面中唯一的命名指导是 /instrumented-tests 的反引号注意事项:“注意:在 Kotlin 中使用反引号命名测试仅在运行 API 30 及以上的设备上受支持。”含义:本地测试(src/test/)用反引号安全;仪器测试(src/androidTest/)要求测试 APK 的 minSdk >= 30,否则使用驼峰或下划线命名。skydoves 约定为 methodName_state_expected,读起来像句子,例如 loadUsers_emptyList_emitsEmptyState。
Hilt 测试机制
单元测试不需要 Hilt(构造注入的类可直接实例化);集成测试中 Hilt 像生产代码一样注入依赖,且无需维护,因为 Hilt 会为每个测试自动生成新组件。
@HiltAndroidTest + HiltAndroidRule 必须最先生效:任何使用 Hilt 的 UI 测试都要加 @HiltAndroidTest,并添加 HiltAndroidRule。要用 @Inject 做字段注入,并在 @Before 中调用 hiltRule.inject()。HiltAndroidRule 必须早于任何触碰注入图的规则(Activity 启动、Compose 设置)。JUnit4 中 order 值更小者先执行,常见写法是 Hilt 用 order = 0、下一个规则用 order = 1,但也可用负数,只要保证 Hilt 在前。
@TestInstallIn 可为模块内所有 @HiltAndroidTest 替换绑定(组件指定为 SingletonComponent::class,replaces 指定被替换模块)。@UninstallModules 只在单个测试中替换绑定。警告:不可卸载未标注 @InstallIn 的模块,否则编译错误;@UninstallModules 只能卸载 @InstallIn 模块,不能卸载 @TestInstallIn 模块。@BindValue 是轻量形式,用于单个测试快速替换单个实例,无需测试模块类。当生产 Application 继承非 Hilt 基类(如 MultiDexApplication)时,使用 @CustomTestApplication(BaseApplication::class) 生成测试应用。组合多个规则时可用 order 排序,或用 RuleChain.outerRule(...).around(...) 组织。
模式(错误 vs 正确)
Given-When-Then 注释块:错误写法把搭建、动作、断言混在一起,读者难以定位失败;正确写法用注释分三段,如先在 Given 中备好带 Alice 的假仓库,When 中加载该 id,Then 断言状态为 Loaded(name = "Alice")。
Hilt 规则顺序:错误写法不设 order,JUnit 顺序不确定,Activity 可能在 Hilt 组件构建前启动并在 @Inject 处抛错;正确写法 Hilt 用 order = 0,Compose/Activity 规则用 order = 1。
非确定性时间:错误写法用 Instant.now().minusSeconds(60) 依赖真实墙钟;正确写法用固定时间 Clock.fixed 并注入。
@BindValue 与 every { }:错误写法尝试对 Hilt 注入的真实实例打桩(会造成测试改动生产状态,且无法编译);正确写法在绑定边界替换,用 @UninstallModules 加 @BindValue 提供假实现。
强制规则
必须让 HiltAndroidRule 先于任何触碰注入图的规则执行;对含 @Inject 字段的测试类必须在 @Before 调用 hiltRule.inject();不得对 @TestInstallIn 替换过的模块使用 @UninstallModules;不得把确定性指导归于 /training/testing/fundamentals/strategies,应使用 /training/testing/instrumented-tests/stability;不得声称 Google 推荐 Given-When-Then。