mcpField
A
android-testing

android-testing

github.com/rcosteira79测试与质量169145 次安装6 周前更新
代码仓库

关于 android-testing

面向 Android 测试的参考技能,重点介绍测试先行、Compose 测试和 Kotlin Multiplatform 易错点。帮助开发者选择合适的测试形式,并处理协程调度器、原生目标编译和截图测试问题。

主要功能

  1. 01RED-GREEN-REFACTOR 测试流程
  2. 02KMP 测试名称与目标检查
  3. 03Compose 测试调度器处理
  4. 04优先使用语义进行 UI 选择
  5. 05UI、集成与截图测试选择
  6. 06确定性截图测试准备

适用场景

  1. 01在编写生产代码前验证行为
  2. 02排查 iOS 目标的 commonTest 编译错误
  3. 03测试 Compose 协程与动画
  4. 04为界面状态建立稳定截图测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rcosteira79/android-skills --skill android-testing

安装后在 Claude Code 中输入/android-testing确认已加载。

详细介绍

Android 测试

以测试优先为基础的 Android 专项测试。本参考聚焦于测试优先原则,以及容易出错的 Compose 测试和 KMP 陷阱,而非三层测试、使用 Fake 而非 Mock、runTest 或 Given-When-Then 命名等基础知识——除非某个名称会导致一个目标平台无法编译。

测试优先(基础)

  • **没有先编写并观察到失败的测试,就不编写生产代码。**先编写测试,观察它失败(RED),再编写能通过测试的最少代码(GREEN),然后重构。没有亲眼观察到失败的行为,就没有得到验证。
  • 只有当一个测试因为某个 Bug 而失败,并且修复后通过时,这个 Bug 才算修复。

这项原则可以叠加在任何专门的 TDD 技能(superpowers:test-driven-development、ace:test-driven-development)之上,但不要求使用它们。要从头搭建测试栈(测试 DI、选择 JUnit/Robolectric/Roborazzi/Paparazzi、插桩测试运行器、Compose Preview Screenshot Testing、UI Automator、Jacoco),请参阅 Google 官方的 testing-setup 技能(android skills add testing-setup)。

commonTest 中的名称必须兼容 Kotlin/Native,而不仅是 JVM

commonTest 会针对每个声明的目标平台分别编译,因此 jvmTest 通过并不能证明 iOS 编译也能通过。反引号在所有目标平台上都没问题——它们是转义语法,不属于名称的一部分——但 Kotlin/Native 前端会拒绝其中包含以下 24 个字符的名称:. ; , ( ) [ ] { } / < > : \ $ & ~ * ? # | § % @(FirNativeIdentifierChecker——自 Kotlin 1.7 起,这是一项无条件错误;这些字符也包含 IR 名称修饰器自身保留的字符)。Kotlin/JVM 只禁止 . ; [ ] / < > : \。逗号是两组限制的差异所在;空格则都不在其中,因此带逗号的 Given-When-Then 命名约定会导致所有 Native 目标平台编译失败,但 CI 仍会通过:

// commonTest — compiles for jvm; fails compileTestKotlinIosSimulatorArm64 with
//   error: name contains illegal characters: ","
@Test fun `given an empty cart, when an item is added, then the total updates`() { … }

// commonTest — same sentence, commas dropped. Backticks and spaces compile everywhere.
@Test fun `given an empty cart when an item is added then the total updates`() { … }

由此得出两条规则。所有 commonTest 声明名称中都不要使用 , . ( ) : ?——检查范围包括类、属性、类型别名、值参数和枚举项,而不仅是 fun。此外,验证 KMP 测试时要使用 allTests 或 <target>Test,绝不要用 jvmTest,因为它只依赖 compileTestKotlinJvm;仅面向设备的目标平台根本没有测试任务,因此要显式编译其测试二进制文件(linkDebugTestIosArm64)。

不要将这一规则与 Kotlin 编码约定提到的 Android 规则混淆:当 minSdk 低于 30 时,D8 会拒绝空格,这只会影响插桩测试——而不会影响逗号,因为 DEX 的 SimpleName 语法在任何 API 级别都不允许逗号。

Compose 测试现在默认使用 StandardTestDispatcher

createComposeRule() / runComposeUiTest {} 默认使用 StandardTestDispatcher(与 kotlinx.coroutines.test.runTest 一致)——**没有单独的“v2”包。**该行为受 androidx.compose.ui.test.ComposeUiTestFlags.isStandardTestDispatcherSupportEnabled 控制(默认值为 true),因此常规的 androidx.compose.ui.test.junit4.createComposeRule 已会使用它。要固定调度器,请将其传入:createComposeRule(effectContext = StandardTestDispatcher())。在此默认行为下,之前会立即执行(旧的 UnconfinedTestDispatcher 行为)的 LaunchedEffect 可能需要显式调用 mainClock.advanceTimeBy(0) / runCurrent() 来处理队列中的任务;只有在需要临时恢复旧行为时,才将该标志设为 false。

双调度器陷阱(值得一提的协程测试注意事项):MainDispatcherRule 的 TestDispatcher 与 runTest { } 创建的调度器使用的是不同的 TestCoroutineScheduler。将 mainRule.dispatcher 传给 runTest(mainRule.dispatcher),这样 Dispatchers.Main 和测试主体才会共享一个调度器——否则 advanceUntilIdle() 只会清空其中一个,断言就会与 ViewModel 产生竞争。

语义优先,testTag 作为后备方案

优先选择面向用户的语义,而不是 testTag ——真实用户和屏幕阅读器能看到语义;除了测试之外,没人能看到 testTag。选择器优先级:(1)onNodeWithText;(2)onNodeWithContentDescription;(3)角色/状态匹配器(hasClickAction()、isSelected()、isFocused()、isEnabled());(4)仅当没有稳定的面向用户的文本,或文本重复/含义不明确时(相同条目的列表、按语言区域变化的文案、多个实例),才使用 onNodeWithTag。文本断言能经受重构并覆盖无障碍体验;testTag 断言会在标签更改时立即失效,还会漏掉面向用户的回归问题。

(值得了解的另一种观点:skydoves/android-testing-skills 主张优先使用标签,以增强国际化(i18n)的稳健性,并以 androidx/material3 自身 1825 : 424 : 46 的 testTag : onNodeWithText : onNodeWithContentDescription 比例作为依据。如果你有单独的无障碍测试,这种做法有其道理;本技能默认语义优先,因为它能捕获一类 testTag 永远无法发现的错误。)

将回调作为测试接口

Composable 的契约是“渲染状态、发出回调”——应准确测试这一点;不要通过 ViewModel mock 来执行断言。

@Test fun `tapping article row invokes onArticleClick with id`() {
    var clickedId: String? = null
    composeTestRule.setContent { ArticleRow(Article(id = "42", title = "Hello"), onArticleClick = { clickedId = it }) }
    composeTestRule.onNodeWithText("Hello").performClick()
    assertEquals("42", clickedId)
}

如何选择测试形式——用能证明行为的最小契约

证明内容形式
渲染的文本、条件内容、加载/错误分支、回调连线普通 UI Compose 测试(状态 + 回调,不包含图)
焦点导航、键盘、电视/D-pad使用 performKeyInput + assertIsFocused() 的 Compose 测试(参见 compose/references/focus-navigation.md)
语义无法证明的视觉契约——间距、主题颜色、字体排版、阴影、渐变、骨架屏截图测试,每种有意义的状态测一次
状态持有器能否正确更新 UI状态持有器单元测试 + 一次连线冒烟测试
生命周期、导航或 DI 集成本身集成测试(createAndroidComposeRule、Hilt 规则、真实依赖图)

**测试时钟与墙上时钟:**对于任何 Compose 状态可观测的条件,使用 mainClock.advanceTimeUntil(ms) { state.value == Done }(确定且快速);只有对于非 Compose 条件(Job.isCompleted、外部计数器)才使用 rule.waitUntil(ms) { … }。在同一测试中混用两者是常见的不稳定因素。

动画测试必须在 setContent 之前设置 mainClock.autoAdvance = false ——否则,框架的 InfiniteAnimationPolicy 会在不确定时长的动画上抛出 CancellationException,而有限时长的动画会在一次自动推进中结束,无法观察到中间状态。暂停后,先用 advanceTimeByFrame()(启动动画)驱动帧,再用 advanceTimeBy(durationMillis) 推进时间。

**确保截图结果确定:**使用固定的状态数据(截图路径中不包含当前时间、随机种子或远程 URL);冻结时钟(Clock.fixed(...))和动画进度;对于图片较多的屏幕,使用假图片加载器(Coil 3 的测试替身:LocalImageLoader provides FakeImageLoader(LocalContext.current),或 setContentWithFakeImageLoader { … });每种有意义的状态(加载/错误/成功/空)截一张,而不是为每个 UI 元素各截一张。

评论

支持 Markdown