mcpField
A
auditing-compose-test-suite

auditing-compose-test-suite

github.com/skydoves测试与质量33969 次安装22 周前更新
代码仓库

关于 auditing-compose-test-suite

这项索引型 Skill 按六个阶段审查现有 Jetpack Compose 测试,并将问题指向对应的专项 Skill。面向检查测试配置与可靠性的开发者;它只报告问题,不修改代码。

主要功能

  1. 01按顺序执行六阶段审查
  2. 02检查配置、查找器、断言、操作、时序与诊断
  3. 03将问题转交给对应专项 Skill
  4. 04不修改代码

适用场景

  1. 01审查现有 Compose 测试文件
  2. 02排查 CI 测试不稳定或过慢
  3. 03在 Compose 发布前检查测试
  4. 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. 首先处理阶段 1 的设置阻碍。 测试位于错误的 source set,或混用了不同测试环境时,行为未定义;在此基础上修复阶段 5 的空闲状态问题只是白费功夫。
  2. 先处理阶段 5 的空闲状态问题,再处理阶段 4 的操作问题。 如果 Thread.sleep 掩盖了缺失的 IdlingResource,阶段 4 的操作问题就会被隐藏;空闲状态正确后,操作时序问题才会清晰显现。
  3. 先处理阶段 2 的查找器问题,再处理阶段 3 的断言问题。 如果查找器定位了错误节点,后续所有断言都毫无意义。
  4. 阶段 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,并以重新路由说明结束审查。

参考资料

评论

支持 Markdown