关于 auditing-compose-test-suite
这项索引型 Skill 按六个阶段审查现有 Jetpack Compose 测试,并将问题指向对应的专项 Skill。面向检查测试配置与可靠性的开发者;它只报告问题,不修改代码。
主要功能
- 01按顺序执行六阶段审查
- 02检查配置、查找器、断言、操作、时序与诊断
- 03将问题转交给对应专项 Skill
- 04不修改代码
适用场景
- 01审查现有 Compose 测试文件
- 02排查 CI 测试不稳定或过慢
- 03在 Compose 发布前检查测试
- 04评估迁移到 StandardTestDispatcher
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ android-testing-skills --skill auditing-compose-test-suite安装后在 Claude Code 中输入/auditing-compose-test-suite确认已加载。
详细介绍
Compose 测试套件审查——六阶段检查,然后路由
这是一项索引型技能。它会按固定顺序协调 compose-test-skills/ 中的其余技能,对现有测试文件或测试套件进行审查。每个阶段都会生成发现项;每项发现都会指向负责修复的同级技能。审查过程本身绝不会修改代码——这是被路由到的技能的工作。
何时使用此技能
- 用户粘贴一个
*Test.kt文件(或多个文件),并询问“这里有什么问题”“审查一下”或“检查一下”。 - 用户报告系统性间歇性故障、CI 缓慢,或“测试在本地通过,但在 CI 上失败”。
- 用户准备发布 Compose 功能,并希望对测试套件进行审查。
- 用户正从 v1(
UnconfinedTestDispatcher)迁移到 v2(StandardTestDispatcher),并希望获得一份可能已失效的测试检查清单。 - 用户从其他团队接手了测试文件,不确定哪些做法正确,哪些只是碰巧能用。
何时不使用此技能
- 用户遇到特定断言失败并希望修复——直接跳转到相关同级技能(遗漏匹配器通常对应
../../debug/printing-the-semantics-tree/SKILL.md,动画间歇性故障通常对应../../synchronization/testing-animations-deterministically/SKILL.md)。 - 用户要从头开始编写全新的测试——先从
../../setup/configuring-test-dependencies/SKILL.md开始,然后查看../../setup/choosing-test-rule-vs-runtest/SKILL.md,再查看../../setup/setting-up-host-vs-device-tests/SKILL.md。 - 用户希望审查生产环境 composable 的性能——这属于
compose-performance-skills,不属于本仓库。
前提条件
- 可以读取待审查的完整测试文件。单个测试方法范围太窄,无法进行审查——模式通常会在整个类中显现。
- 模块的
build.gradle.kts(用于阶段 1 的依赖检查),以及测试使用自定义 Activity 时的src/androidTest/AndroidManifest.xml。 - 熟悉
../docs/SPEC.md§5 中记录的 skydoves 经验法则(在生产代码中使用 testTag、默认useUnmergedTree = false、动画使用mainClock.autoAdvance = false、waitUntil使用墙上时钟、状态变更使用runOnIdle、优先使用 v2 入口、Thread.sleep是一种坏味道)。
审查流程
按顺序执行六个阶段。一旦某个阶段发现问题,就停止并路由到对应的同级技能——较早阶段的修复有时会使后续阶段的发现失效(例如,在阶段 1 从 v1 切换到 v2,可能会改变阶段 5 的空闲行为)。
阶段 1——设置正确性
检查构建、@Rule 和源集层面:
- Gradle:
androidx.compose.ui:ui-test-manifest是否配置为debugImplementation(设备测试)或testImplementation(主机测试)?ui-test-junit4是否与测试类配置在相匹配的配置项中? - Lint:是否抑制了
TestManifestGradleConfiguration(严重性为 WARNING)? - 规则选择:使用的是
createComposeRule()、createAndroidComposeRule<A>(),还是createEmptyComposeRule()?规则是否与宿主环境匹配?自定义 Activity 是否已在src/androidTest/AndroidManifest.xml中声明? - v1 与 v2 的导入:是否存在任何
androidx.compose.ui.test.junit4.createComposeRule或androidx.compose.ui.test.runComposeUiTest(已弃用 WARNING)? - 混用测试表面:同一个测试类是否同时声明了
@get:Rule val rule = createComposeRule()和runComposeUiTest { }代码块? - 源集:文件位于
src/test/(主机)还是src/androidTest/(设备)?它的能力集是否匹配(主机测试中不能使用captureToImage、enableAccessibilityChecks,也不能进行依赖 ripple 的断言)?
失败情况的路由:
- 缺少或配置错误的依赖、
ActivityNotFoundException、Lint 警告 →../../setup/configuring-test-dependencies/SKILL.md。 - 规则构造函数错误、v1 导入、混用测试表面、重复调用
setContent→../../setup/choosing-test-rule-vs-runtest/SKILL.md。 - 源集错误、在主机上进行截图测试、在主机上执行无障碍检查 →
../../setup/setting-up-host-vs-device-tests/SKILL.md。
阶段 2 — Finder 规范
每次调用 onNode* / onAllNodes* 时要检查的内容:
- Finder 选择:统计
onNodeWithTag、onNodeWithText和onNodeWithContentDescription的使用次数。对非 i18n 字符串大量使用onNodeWithText是脆弱性的信号。Skydoves 的热门观点 #1:Modifier.testTag("…")应通过常量放在生产代码中;测试应按标签查找,而不是按文本查找。 - 生产代码中的标签:匹配的标签是否确实通过生产组合函数中的
Modifier.testTag(...)应用?如果没有,Finder 永远无法匹配。 useUnmergedTree:默认值为false。开发者是否在没有说明理由的情况下更改了它?测试是否依赖合并子树中的标签(例如,Button内部Text上的标签),却匹配到了错误的(父级)节点?- 选择器链:
onChild/onParent会传递useUnmergedTree。任何在遍历过程中切换该标志的链都值得警惕。 onAllNodes(...).filter(...)与自定义SemanticsMatcher:测试是否在重新实现已有的匹配器(hasText、hasClickAction、hasContentDescription)?
故障模式对应:
- 生产代码中缺少标签、文本 Finder 脆弱 →
../../finders/finding-nodes-by-tag-text-content/SKILL.md。 - 合并 / 未合并树选择错误或树导航脆弱 →
../../finders/traversing-the-semantics-tree/SKILL.md。 - 有现成匹配器却手动实现 →
../../finders/composing-semantics-matchers/SKILL.md。 - Finder 在 CI 中找不到任何匹配项 →
../../debug/printing-the-semantics-tree/SKILL.md。
阶段 3 — 断言强度
每次调用 assert* 时要检查的内容:
- 测试作者实际想表达的是
assertIsDisplayed(),却用了assertExists():语义树中存在节点弱于节点在屏幕上可见。 assertCountEquals(n)与assertAny(matcher):测试是否对集合大小过于敏感,而实际意图只是“至少有一个匹配项”?- 边界与状态:
assertWidthIsEqualTo/getUnclippedBoundsInRoot是针对布局的状态检查。将它们与截图断言混用会造成重复工作。 - 文本断言:对用户可见字符串使用
assertTextEquals("Save")会随 i18n 变化;assertTextEquals(context.getString(R.string.save))更稳健。 assertIsOn/assertIsOff/assertIsSelected:这些会查询切换 / 可选语义状态。是否跳过了这些断言,转而使用较弱的assertExists?Dp.assertIsEqualTo的容差:默认为 ½ dp。测试是否在更适合使用容差时断言了精确像素相等?
故障模式对应:
- 过度使用
assertExists、计数断言与意图不符、切换 / 可选状态、文本断言 →../../assertions/asserting-node-state-and-text/SKILL.md。 - 边界检查 →
../../assertions/asserting-bounds-and-dimensions/SKILL.md。
第 4 阶段 — 操作正确性
检查每次 perform* 调用:
performGesture { }:在所有地方都已标记为@Deprecated— 请替换为performTouchInput { }。这一点不可妥协;SPEC 禁止推荐使用performGesture。- 坐标系:
performTouchInput { down(Offset(x, y)) }使用节点局部坐标。测试如果计算全局坐标并将其传给相对节点的 DSL,会导致点击落在屏幕外且无声地失效。 - 触摸 DSL 扩展:测试是否在用原始的
down/moveBy/up调用重新实现click()/longClick()/swipeUp()/pinch(...)?如果有高层扩展,请使用它们。 - 文本输入:
performTextInput("foo")会追加;performTextReplacement("foo")会替换;performTextClearance()会清空。选错会得到"fooSave",而不是"foo"。 - IME 操作:
performImeAction()会触发KeyboardOptions.imeAction设置的操作。正确做法是使用performImeAction()时,测试是否用了Espresso.pressBack()或按键事件? - 多模态:任何混用
performTouchInput和performMouseInput的测试,都应考虑使用performMultiModalInput { touch { … }; mouse { … } },以获得单一指针事件时间线。 - 滚动:
performScrollToNode、performScrollToIndex、performScrollToKey都支持 LazyList — 测试是否还在用蛮力swipeUp?
失败模式对应:
- 已弃用的
performGesture、坐标混淆、滑动原始调用 →../../actions/injecting-touch-gestures/SKILL.md。 - 文本输入/替换/IME 操作 →
../../actions/entering-text/SKILL.md。 - 鼠标、按键、多模态 →
../../actions/injecting-mouse-and-keyboard/SKILL.md。 - 滚动容器(
performScrollToIndex/Key/Node)、performClick→../../actions/clicking-and-scrolling/SKILL.md。 - 更高层级的 LazyList 模式(可见性、
state.layoutInfo、动画条目放置)→../../patterns/testing-lazy-lists/SKILL.md。
第 5 阶段 — 时间与空闲状态正确性
这是最容易出现不稳定问题的阶段。端到端检查每个测试方法:
Thread.sleep(N):除截图测试等待 RenderThread 完成波纹动画外,任何地方都禁止使用。Skydoves 热门观点 #7。在其他任何地方,都应替换为mainClock.advanceTimeBy(durationMs)(测试时钟)或rule.waitUntil(timeoutMillis) { … }(墙上时钟)。- 动画测试:任何会触发动画的测试,都必须在
setContent之前设置mainClock.autoAdvance = false。否则,已安装的InfiniteAnimationPolicy会在无限动画刚启动时就以CancellationException取消它们。Skydoves 热门观点 #3。 - 测试时钟与墙上时钟:
mainClock.advanceTimeUntil { … }使用测试时钟;rule.waitUntil(timeoutMillis) { … }使用墙上时钟。在同一个测试中混用会导致不稳定 —waitUntil会超时,而advanceTimeUntil则会等待永远不会到来的时钟滴答。Skydoves 热门观点 #4。 - 线程外状态变更:在测试线程中执行任何
state.value = X(不在runOnIdle/runOnUiThread内)都会与重组器发生竞争。Skydoves 热门观点 #5。 - v2 排队任务:迁移到
androidx.compose.ui.test.v2.*后,LaunchedEffect会在StandardTestDispatcher下排队。之前依赖UnconfinedTestDispatcher立即调度的测试,可能需要显式调用mainClock.advanceTimeBy(0)或runCurrent()来清空队列。 waitUntil超时:默认值为 1000 ms — 测试是否使用了更长的超时来掩盖缺失的IdlingResource或错误的时钟问题?
失败模式对应:
Thread.sleep、缺少IdlingResource、waitUntil混淆、线程外状态变更、用于外部工作的自定义IdlingResource→../../synchronization/synchronizing-with-idle/SKILL.md。- 帧舍入、
advanceTimeBy粒度、v1/v2 调度器迁移 →../../synchronization/controlling-the-test-clock/SKILL.md。 - 动画不稳定问题、缺少
autoAdvance、InfiniteAnimationPolicy的CancellationException→../../synchronization/testing-animations-deterministically/SKILL.md。
阶段 6 — 调试输出
检查测试失败时显示的内容是否合理:
printToLog/printToString:查找器未找到目标时,测试是否会打印语义树,让失败原因一目了然?还是只抛出AssertionError: failed: assertExists?fetchSemanticsNode:是否有直接调用?它是一个逃生通道,偶尔使用没有问题,但如果到处都是,就说明测试绕过了公开的匹配器 API。- 失败消息:针对集合的匹配器断言是否使用默认错误消息(列出所有匹配/未匹配的节点),还是被自定义的
try/catch吞掉了? - 测试名称:失败的测试名称是否能告诉开发者预期发生什么?还是像
testButton1这样含糊不清?
以下失败模式对应的处理方式:
- 失败原因不明确、查找未命中时缺少树转储、过度使用
fetchSemanticsNode→../../debug/printing-the-semantics-tree/SKILL.md。 - 无障碍检查覆盖缺口、Robolectric 无操作带来的意外 →
../../debug/enabling-accessibility-checks/SKILL.md。
审计报告模板
以 Markdown 表格形式生成审计报告,方便用户粘贴到 PR 描述中。每个发现占一行。按阶段顺序执行,但同一阶段内的发现可按严重程度重新排序。
| # | Phase | File:line | Severity | Finding | Routed skill |
|---|---|---|---|---|---|
| 1 | 1 setup | build.gradle.kts:42 | WARN | ui-test-manifest on androidTestImplementation | ../../setup/configuring-test-dependencies/SKILL.md |
| 2 | 1 setup | FeedTest.kt:18 | WARN | v1 createComposeRule import (deprecated) | ../../setup/choosing-test-rule-vs-runtest/SKILL.md |
| 3 | 2 finder | FeedTest.kt:34 | ERROR | onNodeWithText("Save") — no testTag in production | ../../finders/finding-nodes-by-tag-text-content/SKILL.md |
| 4 | 4 action | FeedTest.kt:51 | ERROR | performGesture { } — deprecated | ../../actions/injecting-touch-gestures/SKILL.md |
| 5 | 5 idle | FeedTest.kt:66 | ERROR | Thread.sleep(2000) before assertion | ../../synchronization/synchronizing-with-idle/SKILL.md |
| 6 | 5 idle | FeedTest.kt:88 | ERROR | autoAdvance not disabled before animation | ../../synchronization/testing-animations-deterministically/SKILL.md |
| 7 | 6 debug | FeedTest.kt:104 | NIT | assertExists with no printToLog on miss | ../../debug/printing-the-semantics-tree/SKILL.md |
严重级别(严格使用以下三个):
- ERROR — 测试已损坏、不稳定,或断言了错误的内容。合并前必须修复。
- WARN — 测试可以运行,但存在可能导致不稳定或回归的异味。如果修复成本低,应在本次迭代中修复。
- NIT — 风格/可维护性问题。在修改相邻代码时修复。
不要自动修复。将报告交给用户;由用户(或后续针对所指向技能的调用)应用修复。
模式
模式:按影响范围排序发现
当同一个 *Test.kt 中有多个发现时,按以下顺序排列:
- 首先处理阶段 1 的设置阻碍。 测试位于错误的 source set,或混用了不同测试环境时,行为未定义;在此基础上修复阶段 5 的空闲状态问题只是白费功夫。
- 先处理阶段 5 的空闲状态问题,再处理阶段 4 的操作问题。 如果
Thread.sleep掩盖了缺失的IdlingResource,阶段 4 的操作问题就会被隐藏;空闲状态正确后,操作时序问题才会清晰显现。 - 先处理阶段 2 的查找器问题,再处理阶段 3 的断言问题。 如果查找器定位了错误节点,后续所有断言都毫无意义。
- 阶段 6 的调试输出问题始终最后修复。 它能改善问题诊断,但不会改变正确性。
模式:优先搜索高信号异味
逐行阅读测试之前,先运行以下 grep 命令,找出影响重大的异味:
# Phase 1 — v1 imports (deprecated)
grep -RIn 'import androidx.compose.ui.test.junit4.createComposeRule' .
grep -RIn 'import androidx.compose.ui.test.runComposeUiTest' .
# Phase 1 — manifest dep on the wrong configuration
grep -RIn 'ui-test-manifest' build.gradle.kts settings.gradle.kts buildSrc/
# Phase 2 — finder usage ratio (skydoves hot take #1: tag-heavy is correct)
grep -RIcn 'onNodeWithTag\|onNodeWithText\|onNodeWithContentDescription' src/
# Phase 4 — deprecated gesture API
grep -RIn 'performGesture' src/
# Phase 5 — Thread.sleep smell
grep -RIn 'Thread.sleep' src/
# Phase 5 — animation tests missing autoAdvance = false
grep -RIn 'animateFloatAsState\|animateDpAsState\|animateColorAsState\|rememberInfiniteTransition' src/test src/androidTest
# Phase 5 — off-thread mutation pattern (heuristic — review hits manually)
grep -RIn '\.value = ' src/test src/androidTest | grep -v 'runOnIdle\|runOnUiThread'
在仔细阅读之前,这些 grep 的输出通常就能构成典型审计报告中 60-80% 的内容。
模式:何时停止审计并重新路由
如果阶段 1 发现测试位于错误的 source set(例如,截图测试放在 src/test/ 中),立即停止。不要执行阶段 2-6。测试移到 src/androidTest/ 后将会整体重写——审计当前版本是在浪费精力。转交给 ../../setup/setting-up-host-vs-device-tests/SKILL.md,并在移动后重新运行审计。
同样,如果阶段 1 发现 runComposeUiTest { } 代码块与 @get:Rule createComposeRule() 位于同一个类中,也应停止并转交给 ../../setup/choosing-test-rule-vs-runtest/SKILL.md。混合环境会让之后的所有发现都难以判定。
模式:向用户呈现审计结果
审计结果以简短的文字总结开头,然后附上表格。总结应点出最重要的三个发现,以及用户接下来应运行的唯一一个同级技能。例如:
The FeedTest.kt suite has 7 findings. The two ERRORs that block correctness are
(1) a Thread.sleep(2000) before the search assertion (FeedTest.kt:66) and
(2) an animation test without mainClock.autoAdvance = false (FeedTest.kt:88).
Run ../../synchronization/testing-animations-deterministically/SKILL.md first;
the autoAdvance fix typically resolves the Thread.sleep finding by exposing
a mainClock.advanceTimeBy(durationMs) replacement.
强制规则
- 不得在审查期间修改代码。审查只负责产出问题发现;修复由路由指定的同级技能执行。
- 必须按顺序执行六个阶段。前面阶段的修复会使后续阶段的问题发现失效;按此顺序可减少无效工作。
- 必须将每项问题发现路由到一个同级技能。未路由到技能的问题发现不可执行,应从报告中省略。
- 必须将所有不在截图/RenderThread 上下文中的
Thread.sleep标记为 ERROR。Skydoves 热点观点 #7。 - 必须将所有未设置
mainClock.autoAdvance = false的动画测试标记为 ERROR。Skydoves 热点观点 #3。 - 必须将所有 v1 入口点(
androidx.compose.ui.test.junit4.createComposeRule、androidx.compose.ui.test.runComposeUiTest)标记为 WARN。Skydoves 热点观点 #6。 - 必须将所有
performGesture { }用法标记为 ERROR。该 API 已标记为@Deprecated;SPEC 禁止推荐使用它。 - 不得捏造问题发现。如果某个阶段没有发现问题,应写“Phase N: no findings”——误报会降低审查的信号价值。
- 必须使用审查报告表格格式(Phase、File:line、Severity、Finding、Routed skill),以便后续技能解析输出。
- **优先:**当多项问题发现集中在同一症状附近(例如“测试在 CI 上不稳定”)时,应在文字摘要中指出该症状,并调整表格顺序,使路由到的同级技能优先显示。
验证
- 每个审查阶段均已执行,并产出问题发现或“no findings”。
- 每项问题发现都有 Severity(ERROR | WARN | NIT)和路由到的同级技能路径。
- 审查本身未修改正在审查的测试文件中的代码。
- 已运行 Phase 1 grep 集,并且其输出已反映在表格中。
- 审查报告表格可编译为有效 Markdown(用户可以将其粘贴到 PR 中)。
- 如果触发了 Phase 1 设置阻塞项(源集错误、表面混杂),则跳过 Phases 2-6,并以重新路由说明结束审查。
参考资料
- Compose 测试概述(Android Developers):https://developer.android.com/develop/ui/compose/testing
- Compose 测试速查表:https://developer.android.com/develop/ui/compose/testing-cheatsheet
- Compose 测试设置:https://developer.android.com/develop/ui/compose/testing#setup
- 测试动画:https://developer.android.com/develop/ui/compose/animation/testing
- Compose 中的语义:https://developer.android.com/develop/ui/compose/accessibility/semantics
- Compose UI 发行说明:https://developer.android.com/jetpack/androidx/releases/compose-ui
- 同级设置技能:
../../setup/configuring-test-dependencies/SKILL.md、../../setup/choosing-test-rule-vs-runtest/SKILL.md、../../setup/setting-up-host-vs-device-tests/SKILL.md。 - 同级查找器技能:
../../finders/finding-nodes-by-tag-text-content/SKILL.md、../../finders/composing-semantics-matchers/SKILL.md、../../finders/traversing-the-semantics-tree/SKILL.md。 - 同级断言技能:
../../assertions/asserting-node-state-and-text/SKILL.md、../../assertions/asserting-bounds-and-dimensions/SKILL.md。 - 同级操作技能:
../../actions/clicking-and-scrolling/SKILL.md、../../actions/injecting-touch-gestures/SKILL.md、../../actions/injecting-mouse-and-keyboard/SKILL.md、../../actions/entering-text/SKILL.md。 - 同级同步技能:
../../synchronization/controlling-the-test-clock/SKILL.md、../../synchronization/testing-animations-deterministically/SKILL.md、../../synchronization/synchronizing-with-idle/SKILL.md。 - 同级模式技能:
../../patterns/structuring-a-compose-test/SKILL.md、../../patterns/testing-lazy-lists/SKILL.md、../../patterns/testing-state-restoration/SKILL.md。 - 同级互操作技能:
../../interop/testing-with-espresso-interop/SKILL.md。 - 同级调试技能:
../../debug/printing-the-semantics-tree/SKILL.md、../../debug/enabling-accessibility-checks/SKILL.md。 - skydoves — compose-performance-skills(用于生产端审计的同级仓库):https://github.com/skydoves/compose-performance-skills。