关于 choosing-test-rule-vs-runtest
此技能帮助开发者为 Compose UI 测试选择 JUnit4 TestRule 或挂起式 runComposeUiTest 入口,并区分适用场景。它还说明 v1 到 v2 的迁移、Activity 选择及避免混用独立测试环境的要点。
主要功能
- 01比较规则式与挂起式入口
- 02按 Activity 类型选择构造器
- 03迁移至 v2 API
- 04避免在同一测试中混用入口
- 05配置 effectContext 调度器
适用场景
- 01为新 Compose UI 测试选择入口
- 02迁移已弃用的 v1 测试 API
- 03测试由 Activity 自行设置内容的界面
- 04为 LaunchedEffect 测试注入调度器
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ android-testing-skills --skill choosing-test-rule-vs-runtest安装后在 Claude Code 中输入/choosing-test-rule-vs-runtest确认已加载。
详细介绍
选择 TestRule 还是 runComposeUiTest — 每个测试只使用一个入口
Compose 提供了两个并行的测试入口:JUnit4 TestRule(createComposeRule() 等)和多平台挂起 lambda(runComposeUiTest { })。两者都会各自独立地设置 recomposer、MainTestClock 和 IdlingResource。在同一个测试中混用会导致难以诊断的双重环境问题。本指南帮你选出合适的入口,并指出影响每个现有代码库的 v1 → v2 弃用变更。
何时使用本指南
- 用户正在编写新的 Compose UI 测试,并在
createComposeRule()和runComposeUiTest { }之间做选择。 - IDE 在
import androidx.compose.ui.test.junit4.createComposeRule或import androidx.compose.ui.test.runComposeUiTest上显示弃用WARNING。 - 用户报告称,测试使用 v1 入口时通过,但迁移后因
LaunchedEffect不再同步运行而失败。 - 用户有自定义的
ComponentActivity子类,不确定该使用createAndroidComposeRule还是createEmptyComposeRule。 - 用户报告
IllegalStateException: setContent can only be called once per ComposeTestRule,或“已启动的 Activity 已调用 setContent”。 - 用户正在编写 Compose Multiplatform (KMP) 测试,需要使用挂起入口。
何时不使用本指南
- 依赖项尚未编译通过——请先查看
./configuring-test-dependencies/SKILL.md。 - 需要选择的是主机测试还是设备测试,而不是 rule 还是 lambda——请参阅
./setting-up-host-vs-device-tests/SKILL.md。 - 测试可以编译和运行,但在空闲状态或动画方面不稳定——请参阅
../../synchronization/synchronizing-with-idle/SKILL.md和../../synchronization/testing-animations-deterministically/SKILL.md。
前置条件
- 在正确的配置中添加
androidx.compose.ui:ui-test、androidx.compose.ui:ui-test-junit4,以及(对于createComposeRule()/runComposeUiTest { })androidx.compose.ui:ui-test-manifest——请参阅./configuring-test-dependencies/SKILL.md。 - 如果开发者选择 rule 路径,测试类的类路径中必须包含 JUnit4。
- 了解
MainTestClock和 recomposer 共用一个kotlinx.coroutines.test.TestCoroutineScheduler。
工作流
-
1. 在 JUnit4 rule 和挂起 lambda 之间做选择。
路径 适用情况 createComposeRule()/createAndroidComposeRule<A>()代码库已使用 JUnit4 rules;开发者希望通过 RuleChain链接一个 rule(例如HiltAndroidRule);测试仅针对 Android。runComposeUiTest { }/runAndroidComposeUiTest<A> { }Compose Multiplatform;开发者希望使用 effectContext: CoroutineContext注入TestDispatcher;测试从头到尾都是挂起测试。两种方式都提供相同的匹配器、查找器、操作和
MainTestClock。rule 路径对现有 Android 工程师更熟悉;多平台项目长期建议使用 lambda 路径。 -
2. 为 JUnit4 路径选择正确的 rule 构造函数。
构造函数 返回值 宿主 是否提供 setContent?createComposeRule()ComposeContentTestRuleandroidx.activity.ComponentActivity(来自ui-test-manifest)是 createAndroidComposeRule<A : ComponentActivity>()AndroidComposeTestRule<ActivityScenarioRule<A>, A>自定义 Activity A(由开发者在测试 manifest 中声明)是 createEmptyComposeRule()ComposeTestRule(不是ComposeContentTestRule)无——开发者自行启动 scenario 否 来源:
compose/ui/ui-test-junit4/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvmAndAndroid.kt声明了interface ComposeContentTestRule : ComposeTestRule { fun setContent(...) };createEmptyComposeRule()只返回父类型。 -
3. 为 lambda 路径选择正确的挂起入口。
函数 接收者 宿主 runComposeUiTest { }ComposeUiTestComponentActivityrunAndroidComposeUiTest<A> { }AndroidComposeUiTest<A>(添加val activity: A?)自定义 Activity ArunEmptyComposeUiTest { }ComposeUiTest无 — setContent会抛出IllegalStateExceptionrunComposeUiTest和runAndroidComposeUiTest接受effectContext: CoroutineContext = EmptyCoroutineContext、runTestContext: CoroutineContext = EmptyCoroutineContext和testTimeout: Duration = 60.seconds。runEmptyComposeUiTest是例外 — 它只接受block: ComposeUiTest.() -> Unit(没有调度参数);它的用途是让测试在代码块中启动自己的ActivityScenario。来源:compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt:184, 232, 352(v1)以及 v2 的 actual 定义…/v2/ComposeUiTest.android.kt:75, 122, 238。 -
4. 优先使用 v2 入口,而非 v1。 新代码必须遵循此要求。v1 形式(
androidx.compose.ui.test.junit4.createComposeRule、androidx.compose.ui.test.runComposeUiTest)带有@Deprecated(level = DeprecationLevel.WARNING)。runComposeUiTest的弃用消息(摘自ComposeUiTest.android.kt原文)为:
"Use `androidx.compose.ui.test.v2.runComposeUiTest` instead. The v2 APIs align with
standard coroutine behavior by queuing tasks rather than executing them
immediately. Tests relying on immediate execution may require explicit
synchronization. Please refer to the migration guide for more details."
createComposeRule() 带有类似但独立的弃用消息(位于 ComposeTestRule.jvmAndAndroid.kt:347-355),指向 androidx.compose.ui.test.junit4.v2.createComposeRule。这两条消息并非逐字相同 — 请引用与开发者实际迁移的 API 相对应的消息。
迁移映射:
| v1(已弃用 WARNING) | v2(推荐) |
|---|---|
androidx.compose.ui.test.junit4.createComposeRule | androidx.compose.ui.test.junit4.v2.createComposeRule |
androidx.compose.ui.test.junit4.createAndroidComposeRule | androidx.compose.ui.test.junit4.v2.createAndroidComposeRule |
androidx.compose.ui.test.junit4.createEmptyComposeRule | androidx.compose.ui.test.junit4.v2.createEmptyComposeRule |
androidx.compose.ui.test.runComposeUiTest | androidx.compose.ui.test.v2.runComposeUiTest |
androidx.compose.ui.test.runAndroidComposeUiTest | androidx.compose.ui.test.v2.runAndroidComposeUiTest |
androidx.compose.ui.test.runEmptyComposeUiTest | androidx.compose.ui.test.v2.runEmptyComposeUiTest |
行为差异:v1 使用 UnconfinedTestDispatcher(立即执行),v2 使用 StandardTestDispatcher(排队执行)。迁移后,依赖 LaunchedEffect / rememberCoroutineScope 代码块同步运行的测试,可能需要显式调用 mainClock.advanceTimeBy(0) 或 runCurrent() 来处理排队任务。这是唯一常见的破坏性变更。
- 5. 切勿在同一测试中混用这两个接口。 二者分别管理独立的测试环境。
runComposeUiTest的 KDoc(ComposeUiTest.android.kt第 157-160 行)明确指出:
"Keeping a reference to the [ComposeUiTest] outside of this function is an error. Also avoid
using [androidx.compose.ui.test.junit4.ComposeTestRule] (e.g., createComposeRule) inside
[runComposeUiTest][block] or any of their respective variants. Since these APIs independently
manage the test environment, mixing them may lead to unexpected behavior."
意外混用的症状:setContent 被调用两次、重组在错误的调度器上运行、MainTestClock 在一个环境中推进而另一个环境中没有推进,以及 waitForIdle 因查询了错误的 recomposer 而立即返回。
-
6. 每个测试恰好调用一次
setContent。 第二次调用时,ComposeContentTestRule.setContent和ComposeUiTest.setContent都会抛出IllegalStateException。请通过修改状态触发重组,切勿再次调用setContent。 -
7. 如果启动的 Activity 自行调用
setContent,请勿调用composeTestRule.setContent。 Activity 已经设置了 Compose 内容;调用规则中的方法会覆盖原内容并使之前的树失效。请使用createAndroidComposeRule<MyActivity>(),并通过查找器直接读取现有树。ComposeUiTest.android.kt第 200-204 行的 KDoc 提醒:“如果 Activity 在启动期间设置了内容,就不能再对 ComposeUiTest 使用setContent,因为这会覆盖内容,并可能导致难以察觉的 bug。” -
8. 需要自定义协程语义时,使用
effectContext。 lambda 路径接收一个CoroutineContext,它会成为LaunchedEffect和rememberCoroutineScope作用域的父上下文。来自ComposeUiTest.android.kt:“如果此上下文包含TestDispatcher或TestCoroutineScheduler(按此顺序),它将用于组合和 MainTestClock。” 传入共享的TestDispatcher,以协调测试主体的协程调度器与组合的调度器。JUnit4 规则没有同等的一流挂钩——生产代码大量使用LaunchedEffect时,这确实是优先选择 lambda 路径的理由。 -
9. 默认
testTimeout为 60 秒。 同一文件中的默认值为testTimeout: Duration = 60.seconds。超时的测试会抛出AndroidComposeUiTestTimeoutException。仅按测试覆盖此值;切勿全局延长超时时间来掩盖不稳定测试。
模式
模式:错误与正确 — v1 和 v2 导入
// WRONG — emits @Deprecated WARNING and uses UnconfinedTestDispatcher
import androidx.compose.ui.test.junit4.createComposeRule
class MyTest {
@get:Rule val rule = createComposeRule()
}
// WRONG because: v1 dispatches LaunchedEffect work eagerly. After bumping
// kotlinx-coroutines-test the same test will produce different observable
// behavior than runTest { } in the rest of the codebase.
// RIGHT — v2, StandardTestDispatcher, no deprecation warning
import androidx.compose.ui.test.junit4.v2.createComposeRule
class MyTest {
@get:Rule val rule = createComposeRule()
}
模式:错误与正确 — 混用规则和 lambda
// WRONG
class MyTest {
@get:Rule val rule = createComposeRule() // env #1
@Test
fun bad() = runComposeUiTest { // env #2 — independent recomposer + clock
rule.setContent { App() } // sets content in env #1
onNodeWithTag("save").performClick() // queries env #2 — finds nothing
}
}
// WRONG because: each environment owns its own test scheduler and idling resource.
// The KDoc forbids this configuration explicitly.
// RIGHT — pick one
class MyTest {
@Test
fun good() = runComposeUiTest {
setContent { App() }
onNodeWithTag("save").performClick()
}
}
模式:错误与正确 — 误用 createEmptyComposeRule
// WRONG
@get:Rule val rule = createEmptyComposeRule()
@Test fun broken() {
rule.setContent { App() } // compile error — ComposeTestRule has no setContent
}
// WRONG because: createEmptyComposeRule() returns ComposeTestRule, not ComposeContentTestRule.
// It is for tests that launch their own ActivityScenario.
// RIGHT — launch your own scenario, then query
@get:Rule val rule = createEmptyComposeRule()
@Test fun ok() {
ActivityScenario.launch(MyActivity::class.java).use {
// MyActivity.onCreate calls setContent { App() }
rule.onNodeWithTag("save").performClick()
}
}
模式:错误与正确 — Activity 已设置内容
// WRONG
@get:Rule val rule = createAndroidComposeRule<MainActivity>()
@Test fun bad() {
rule.setContent { App() } // throws IllegalStateException — MainActivity.onCreate already set content
}
// RIGHT
@get:Rule val rule = createAndroidComposeRule<MainActivity>()
@Test fun good() {
rule.onNodeWithTag("save").performClick() // query content the Activity already installed
}
模式:使用自定义调度器的 lambda 路径
@OptIn(ExperimentalTestApi::class, ExperimentalCoroutinesApi::class)
@Test
fun launchedEffectFlow() = runComposeUiTest(
effectContext = StandardTestDispatcher(), // share scheduler with composition
) {
setContent { ScreenWithLaunchedEffect() }
mainClock.advanceTimeBy(0) // drain queued LaunchedEffect work
onNodeWithTag("counter").assertTextEquals("1")
}
模式:Hilt 与 Compose 的规则链
@get:Rule(order = 0) val hilt = HiltAndroidRule(this)
@get:Rule(order = 1) val compose = createAndroidComposeRule<HiltTestActivity>()
@Test fun feed() {
hilt.inject()
compose.onNodeWithTag("feed_list").assertIsDisplayed()
}
这是保留 JUnit4 规则路径的典型理由:RuleChain 的顺序。lambda 路径没有同等方式——开发者必须在挂起代码块中手动管理 Hilt 初始化。
强制规则
- 必须在新代码中优先使用 v2 导入(
androidx.compose.ui.test.junit4.v2.*、androidx.compose.ui.test.v2.*),而非 v1。v1 形式的弃用级别为WARNING。 - 不得在同一个测试类中混用
runComposeUiTest { }和@get:RuleComposeTestRule。它们管理相互独立的测试环境。 - 不得在每个测试中多次调用
setContent。两种 API 都会在第二次调用时抛出异常。 - 不得在已自行调用
setContent的已启动 Activity 中调用composeTestRule.setContent。应使用createAndroidComposeRule<A>()并查询现有树。 - 必须根据宿主要求选择对应的规则构造函数:
ComponentActivity使用createComposeRule(),自定义 Activity 使用createAndroidComposeRule<A>(),仅在测试自行启动ActivityScenario时使用createEmptyComposeRule()。 - 必须在
src/androidTest/AndroidManifest.xml(或src/debug/AndroidManifest.xml)中声明createAndroidComposeRule<A>()使用的任何自定义 Activity——参见./configuring-test-dependencies/SKILL.md。 - **优先:**生产代码大量使用
LaunchedEffect或rememberCoroutineScope时,使用effectContext = StandardTestDispatcher()的 lambda 路径,让组合和测试主体共享同一个调度器。 - **优先:**依靠
mainClock.advanceTimeBy(0)(测试时钟)而非runOnIdle { }(墙上时钟加空闲等待)来处理 v2 排队的工作——有关autoAdvance约定,参见../../synchronization/testing-animations-deterministically/SKILL.md。
验证
- 新代码中不再有
import androidx.compose.ui.test.junit4.createComposeRule(v1);应改为导入 v2 路径。 - IDE 未在测试入口点显示
@Deprecated的WARNING。 - 测试类使用
@get:RuleComposeTestRule或runComposeUiTest { }代码块,二者不可兼用。 - 每个测试方法恰好调用一次
setContent(Activity 自行设置内容时则调用零次)。 -
createEmptyComposeRule()仅出现在明确自行启动ActivityScenario的测试中。 - 如果传入
effectContext,测试需通过@OptIn(ExperimentalTestApi::class)(或 v2 等效形式)编译;并在需要处理排队工作的地方添加mainClock.advanceTimeBy(0)。
参考资料
- Compose 测试概览(Android Developers):https://developer.android.com/develop/ui/compose/testing
- Compose 测试速查表:https://developer.android.com/develop/ui/compose/testing-cheatsheet
- Compose Multiplatform 测试:https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html
- Compose UI 发行说明:https://developer.android.com/jetpack/androidx/releases/compose-ui
compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt—runComposeUiTest、runAndroidComposeUiTest、runEmptyComposeUiTest,以及 v1 弃用消息。第 157-160、204、247、338 行的 KDoc 禁止混用规则和 lambda。compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/v2/ComposeUiTest.android.kt— 使用StandardTestDispatcher的 v2runComposeUiTestactual 实现。compose/ui/ui-test-junit4/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvmAndAndroid.kt—interface ComposeTestRule、interface ComposeContentTestRule : ComposeTestRule { fun setContent(...) }。compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt—createComposeRule()、createAndroidComposeRule<A>()、createEmptyComposeRule()的 actual 实现。compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/v2/AndroidComposeTestRule.android.kt— v2 actual 实现(默认使用 StandardTestDispatcher)。