关于 organizing-test-source-sets
面向 Android 开发者的测试技能,说明测试文件的放置位置、对应的 Gradle 配置,以及如何替换旧的 sharedTest 布局。区分本地 JVM、仪器测试和主机/设备测试源集。
主要功能
- 01区分 src/test/ 与 src/androidTest/
- 02说明 testImplementation 和 androidTestImplementation
- 03解释测试清单使用 debugImplementation
- 04介绍 sharedTest 迁移方案
- 05说明 androidHostTest 与 androidDeviceTest
适用场景
- 01为 Robolectric 测试选择源集
- 02修复测试依赖配置错误
- 03在 AGP 7.2 后迁出 sharedTest
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ android-testing-skills --skill organizing-test-source-sets安装后在 Claude Code 中输入/organizing-test-source-sets确认已加载。
详细介绍
整理测试源集 — src/test/、src/androidTest/ 和 KMP 拆分
测试文件所在的位置决定了运行它的 JVM、它能访问哪些依赖,以及它是否会打包进测试 APK。大多数模块只需要 src/test/ 和 src/androidTest/。过去,在主机和设备测试之间共享辅助工具的模块会使用 src/sharedTest/(这是 Codelab 的惯例,并非 Google 官方文档规定的做法),但它在 AGP 7.2+ 中失效,促使 Compose 本身转向 KMP 风格的 androidHostTest / androidDeviceTest 拆分。本技能梳理了这张对照表,让 Agent 能够放心地放置测试文件。
何时使用本技能
- 用户询问“我的测试应该放在哪里?”或“放在
src/test/还是src/androidTest/?”。 - 用户询问“
testImplementation、androidTestImplementation和debugImplementation有什么区别?”。 - 用户报告
Unresolved reference: createComposeRule,且 Agent 怀疑依赖配置错误。 - 用户正在从
src/sharedTest/迁移,因为 AGP 7.2+ 导致其sourceSets { test.java.srcDir(...) }代码块失效。 - 用户提到
androidHostTest/androidDeviceTest,并询问如何在非 KMP 模块中设置它们。 - 用户询问“为什么我的 Robolectric 测试在
src/androidTest/里?”——它应该放在src/test/中。
何时不使用本技能
- 用户正在配置 Compose 测试的 Gradle 依赖(
ui-test-junit4、ui-test-manifest)——请使用../../../compose/setup/configuring-test-dependencies/SKILL.md。 - 用户正在选择测试范围(小型/中型/大型)——请使用
../../concepts/understanding-the-testing-pyramid/SKILL.md。 - 用户正在配置
AndroidJUnitRunner本身——请使用../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md。 - 用户正在为 JVM 测试配置 JUnit4 运行器——请使用
../../../jvm-tests/runner/configuring-junit4-on-android/SKILL.md。
前置条件
- 一个应用了 Android Gradle Plugin(
com.android.application或com.android.library)的模块。 - 熟悉 Gradle 配置名称(
testImplementation、androidTestImplementation、debugImplementation)。 - 了解 JVM 测试在主机的 JVM 上运行,而插桩测试在设备/模拟器上运行。(请参阅
../../concepts/understanding-the-testing-pyramid/SKILL.md。)
Google 文档中介绍的两个源集
src/test/ — 本地 JVM 测试
根据 /training/testing/local-tests:
“默认情况下,本地单元测试的源文件放在
module-name/src/test/中。使用 Android Studio 创建新项目时,此目录已存在。” —developer.android.com/training/testing/local-tests
“本地测试直接在你自己的工作站上运行,而不是在 Android 设备或模拟器上。因此,它使用本地 Java 虚拟机(JVM)。” —
developer.android.com/training/testing/local-tests
由 testImplementation 驱动。通过 ./gradlew test 或 ./gradlew testDebugUnitTest 运行。不需要 Android 设备或模拟器。除非使用 Robolectric 模拟,否则无法使用 Android 框架。
src/androidTest/ — 插桩测试
根据 /training/testing/instrumented-tests:
“在 Android Studio 项目中,插桩测试的源文件存放在
module-name/src/androidTest/java/。创建新项目时,此目录已存在,并且包含一个插桩测试示例。” —developer.android.com/training/testing/instrumented-tests
由 androidTestImplementation 驱动。通过 ./gradlew connectedAndroidTest 或 ./gradlew connectedDebugAndroidTest 运行。需要连接设备或模拟器。根据 CORPUS §H.2 使用 AndroidJUnitRunner。
该页面还明确说明了 testImplementation 与 androidTestImplementation 的区别:
“注意:
testImplementation为本地测试添加依赖,androidTestImplementation为插桩测试添加依赖。” —developer.android.com/training/testing/local-tests
配置矩阵
Configuration Source set Variant Ships in
-----------------------------------------------------------------------------------
implementation main all release + debug + tests
api main all release + debug + tests
debugImplementation debug only debug debug APK + androidTest APK
releaseImplementation release only release release APK only
testImplementation src/test/ all (host) JVM test classpath
testDebugImplementation src/test/ debug (host) JVM test classpath, debug only
androidTestImplementation src/androidTest/ all (device) androidTest APK
androidTestUtil n/a all orchestrator/util install (NOT classpath)
Agent 必须分清以下三种配置:
testImplementation— JVM(主机)测试。包含 Mockito、MockK、Robolectric、kotlinx-coroutines-test、Turbine。androidTestImplementation— 插桩(设备)测试。包含 Espresso、UiAutomator、AndroidJUnitRunner、Composeui-test-junit4。debugImplementation— 主debug变体。androidx.compose.ui:ui-test-manifest和androidx.fragment:fragment-testing-manifest使用此配置,因为测试 APK 构建在 debug APK 之上,需要合并其中的清单条目。
androidx.compose.ui:ui-test-manifest 必须添加到 debugImplementation,而不是 androidTestImplementation——请参阅 ../../../compose/setup/configuring-test-dependencies/SKILL.md,了解强制执行此规则的 lint 检查(TestManifestGradleConfiguration)。
src/sharedTest/ — 社区惯例,并非 Google 官方文档所述
根据 tasks/research/R8-android-fundamentals.md:
在六个范围内的
developer.android.com/training/testing/...页面以及已获取的 Hilt 测试/rules 页面中,都没有明确提到src/sharedTest/。
这种做法源自 Google 自己的 Architecture Blueprints 示例和 Advanced Android Testing Codelab。该模式使用 Gradle sourceSets,将同一个 Java/Kotlin 文件夹注入 test 和 androidTest:
// pre-AGP 7.2 — the classic sharedTest wiring
android {
sourceSets {
getByName("test").java.srcDir("src/sharedTest/java")
getByName("androidTest").java.srcDir("src/sharedTest/java")
}
}
采用这种做法的原因:只编写一次测试,就能在 CI 中将其作为快速的 Robolectric 主机测试运行,也能在模拟器上将其作为高保真插桩测试运行。测试类使用 androidx.test.ext.junit.runners.AndroidJUnit4(根据 CORPUS §G.6,它能在这两种环境中正确分发)。
这种做法失效的原因:AGP 7.2+ 会强制限制同一源目录不能在构建时被多个源集共享——构建本身会直接拒绝,并提示重复的 srcDir(...) 调用。(AGP 在构建时拒绝,而不是在运行时抛出 LinkageError。)相关问题见 https://github.com/robolectric/robolectric/issues/7432(Robolectric 维护者讨论帖,包含复现案例和 AGP 版本表)。许多模块于是:
- 停止共享测试,并接受代码重复。
- 迁移到独立的 Gradle 子模块(
:core-testing),通过testImplementation和androidTestImplementation提供测试辅助代码。 - 采用 KMP 风格的源集名称(
androidHostTest/androidDeviceTest)。
KMP 风格的 androidHostTest/androidDeviceTest
根据 CORPUS §C(“源集约定(KMP)”),Compose UI 测试内部采用这种布局:
commonTest
└── androidCommonTest (helpers shared by host + device)
├── androidDeviceTest (instrumentation APK on emulator/device)
└── androidHostTest (Robolectric on JVM)
androidx 的实际示例:compose/ui/ui-test/src/androidHostTest/kotlin/androidx/compose/ui/test/RobolectricComposeTest.kt 是主机(Robolectric)测试,而 compose/material3/material3/src/androidDeviceTest/.../SwitchTest.kt 是设备测试。两者都基于 commonTest 编译,以使用共享辅助代码。
对于非 KMP 的纯 Android 模块,可以通过独立子模块近似实现相同效果:
// :feature:home/build.gradle.kts — production module
plugins { id("com.android.library") }
dependencies {
testImplementation(project(":feature:home-testing"))
androidTestImplementation(project(":feature:home-testing"))
}
// :feature:home-testing/build.gradle.kts — pure Kotlin or Android-library "test fixtures"
plugins { id("java-library") } // or com.android.library if Android types needed
:feature:home-testing 模块包含 FakeUserRepository、MainDispatcherRule、自定义匹配器等主机和设备测试都需要的内容。
另一种方案:使用 AGP 7.0+ 的 testFixtures 标志(android { testFixtures { enable = true } }),它会公开 testFixtures() Gradle 构件,供 testFixturesImplementation 使用,也可由同级模块的 testImplementation/androidTestImplementation 消费。
不得引用 developer.android.com/training/testing/... 来说明 sharedTest——这些页面都没有记载它。必须改为引用 Architecture Blueprints 仓库或 tasks/research/R8-android-fundamentals.md。
文件放置决策树
Where does this test go?
│
├── Pure Kotlin/JVM logic, no Android framework → src/test/
│ (testImplementation deps)
│
├── Robolectric (Android framework simulated on JVM) → src/test/
│ (testImplementation deps + Robolectric)
│ NOT src/androidTest/.
│
├── ViewModel + coroutines + fakes → src/test/
│
├── Real Room database integration → src/androidTest/
│ (small instrumented test per /fundamentals)
│
├── Espresso UI test → src/androidTest/
├── UiAutomator cross-app test → src/androidTest/
├── Compose UI test against real Activity → src/androidTest/
│ + debugImplementation("...:ui-test-manifest")
│
├── Compose UI test on Robolectric → src/test/
│ (testImplementation("...:ui-test-junit4") + Robolectric)
│
├── Test helper (FakeRepository, MainDispatcherRule) → :core-testing module OR src/sharedTest/
│ (with the AGP 7.2+ caveats above)
模式
模式:错误做法与正确做法 — testImplementation 与 androidTestImplementation
// WRONG — Espresso on testImplementation
dependencies {
testImplementation("androidx.test.espresso:espresso-core:3.7.0")
testImplementation("androidx.test.ext:junit:1.3.0")
}
// WRONG because: Espresso requires a real or simulated Android device. testImplementation
// only puts the JAR on the JVM classpath. The test compiles but ClassNotFoundException /
// IllegalStateException at runtime when Espresso looks for an Instrumentation instance.
// RIGHT — Espresso on androidTestImplementation
dependencies {
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
}
模式:错误做法与正确做法 — 将 Robolectric 放在 src/androidTest/ 中
// WRONG
// src/androidTest/java/com/example/SomeRobolectricTest.kt
@RunWith(RobolectricTestRunner::class)
class SomeRobolectricTest { /* ... */ }
// WRONG because: Robolectric is a JVM Android simulator (per /fundamentals: "Big local test:
// You can use an Android simulator that runs locally, such as Robolectric"). It belongs in
// src/test/. Putting it in src/androidTest/ either fails to find Robolectric (which is on
// testImplementation) or runs it on the device JVM where its shadow mechanism does not work.
// RIGHT
// src/test/java/com/example/SomeRobolectricTest.kt
@RunWith(AndroidJUnit4::class) // dispatches to Robolectric on JVM
@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
class SomeRobolectricTest { /* ... */ }
模式:错误做法与正确做法 — ui-test-manifest 配置
// WRONG
dependencies {
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
androidTestImplementation("androidx.compose.ui:ui-test-manifest") // wrong config
}
// WRONG because: ui-test-manifest contributes <activity android:name="ComponentActivity">
// to the merged debug APK manifest. On androidTestImplementation, the merger does not pick
// it up; createComposeRule() crashes with ActivityNotFoundException. Lint also flags this
// as TestManifestGradleConfiguration (WARNING).
// RIGHT
dependencies {
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
有关 lint 细节,请参阅 ../../../compose/setup/configuring-test-dependencies/SKILL.md。
模式:AGP 7.2+ sharedTest 迁移
// BROKE on AGP 7.2+
android {
sourceSets {
getByName("test").java.srcDir("src/sharedTest/java")
getByName("androidTest").java.srcDir("src/sharedTest/java")
}
}
// MIGRATION OPTION A — extract a :core-testing sub-module
// :core-testing/build.gradle.kts
plugins { id("java-library") }
// classes here are consumed by both test sourcesets in dependent modules
// :feature:home/build.gradle.kts
dependencies {
testImplementation(project(":core-testing"))
androidTestImplementation(project(":core-testing"))
}
// MIGRATION OPTION B — testFixtures
android {
testFixtures { enable = true }
}
// src/testFixtures/java/... contains FakeUserRepository, MainDispatcherRule
// dependent module
dependencies {
testImplementation(testFixtures(project(":core")))
androidTestImplementation(testFixtures(project(":core")))
}
// MIGRATION OPTION C — KMP source-set naming
// In a KMP Android module:
kotlin {
androidTarget {
compilations.getByName("debug") { /* ... */ }
}
sourceSets {
val androidHostTest by getting // Robolectric, JVM
val androidDeviceTest by getting // emulator/device
val androidCommonTest by getting // helpers shared by both
androidHostTest.dependsOn(androidCommonTest)
androidDeviceTest.dependsOn(androidCommonTest)
}
}
Compose 本身在内部使用选项 C(参见 CORPUS §B“源集约定”)。
模块级 Gradle 示例
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.feature.home"
compileSdk = 35
defaultConfig {
minSdk = 23
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
testOptions {
unitTests.isIncludeAndroidResources = true // required for Robolectric
animationsDisabled = true // hermetic instrumented tests
}
}
dependencies {
// production
implementation("androidx.activity:activity-compose")
// host (JVM) tests — src/test/
testImplementation("junit:junit:4.13.2")
testImplementation("androidx.test:core:1.7.0")
testImplementation("androidx.test.ext:junit:1.3.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.0")
testImplementation("org.mockito:mockito-core:5.13.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
testImplementation("org.robolectric:robolectric:4.13")
testImplementation("app.cash.turbine:turbine:1.1.0")
// instrumented (device) tests — src/androidTest/
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
// debug-only — merged manifest entries for test scaffolds
debugImplementation("androidx.compose.ui:ui-test-manifest")
debugImplementation("androidx.fragment:fragment-testing-manifest:1.8.5")
// orchestrator (note: androidTestUtil, not androidTestImplementation)
androidTestUtil("androidx.test:orchestrator:1.6.1")
}
强制规则
- 必须将纯 Kotlin / JVM 测试放在
src/test/中,并使用testImplementation依赖。 - 必须将 Robolectric 测试放在
src/test/中。Robolectric 是 JVM Android 模拟器,而非插桩运行器。 - 必须将真机 / 模拟器测试放在
src/androidTest/中,并使用androidTestImplementation依赖。 - 必须将
androidx.compose.ui:ui-test-manifest和androidx.fragment:fragment-testing-manifest添加到debugImplementation,绝不能添加到androidTestImplementation。 - 禁止引用
developer.android.com/training/testing/...作为src/sharedTest/的依据。该文档并未对此作出说明。请引用 Architecture Blueprints 仓库或tasks/research/R8-android-fundamentals.md。 - 禁止将 Espresso、UiAutomator 或
ui-test-junit4添加到testImplementation。虽然它们可以编译,但在没有Instrumentation的情况下会在运行时失败。 - **优先考虑:**对于共享测试辅助工具,应提取一个
:core-testingGradle 子模块,或使用android.testFixtures.enable = true,而不要使用在 AGP 7.2 中不稳定的sharedTest源集。 - **优先考虑:**在 KMP 模块中遵循 Compose 的做法——辅助工具放在
androidCommonTest中,Robolectric 测试放在androidHostTest中,模拟器测试放在androidDeviceTest中。 - **优先考虑:**在
src/test/中可自由使用反引号命名测试方法;在src/androidTest/中则应按照/training/testing/instrumented-tests将设备限制为 API 30 及以上版本。
验证
-
src/androidTest/下没有 Robolectric 测试。运行find src/androidTest -name '*.kt' -exec grep -l 'org.robolectric' {} +;预期输出为空。 -
src/test/下没有 Espresso / UiAutomator 导入。运行find src/test -name '*.kt' -exec grep -l 'androidx.test.espresso\|androidx.test.uiautomator' {} +;预期输出为空。 -
androidx.compose.ui:ui-test-manifest仅在debugImplementation中声明。运行./gradlew :<module>:lint;预期不会出现TestManifestGradleConfiguration警告。 -
./gradlew :<module>:testDebugUnitTest运行 JVM 测试,且不会调用connectedDebugAndroidTest。 -
./gradlew :<module>:connectedDebugAndroidTest仅调用src/androidTest/下的测试(通过--info输出验证)。 - 如果存在共享辅助工具,它们应位于
:core-testing子模块、testFixtures或androidCommonTest中——绝不能放在通过sourceSets { ... srcDir(...) }接入的字面路径src/sharedTest/下。 - 当任何主机测试引用
R.*时,应设置testOptions.unitTests.isIncludeAndroidResources = true。
参考资料
developer.android.com/training/testing/local-tests—src/test/、testImplementation、“本地测试直接在你自己的工作站上运行”。developer.android.com/training/testing/instrumented-tests—src/androidTest/、androidTestImplementation、AndroidJUnitRunner、反引号注意事项。developer.android.com/training/testing/fundamentals— “大型本地测试:你可以使用在本地运行的 Android 模拟器,例如 Robolectric”。tasks/research/R8-android-fundamentals.md— 原文引述;文档指出,src/sharedTest/并未出现在六个相关的/training/testing/...页面中的任何一个。- Architecture Blueprints(
sharedTest模式的来源):https://github.com/android/architecture-samples - AGP 源集文档:https://developer.android.com/build/build-variants#sourcesets
testFixtures标志:https://developer.android.com/build/dependencies#test-fixtures- CORPUS §C “源集约定(KMP)”——Compose 内部的
androidHostTest/androidDeviceTest布局。 compose/ui/ui-test/src/androidHostTest/kotlin/androidx/compose/ui/test/RobolectricComposeTest.kt— androidx 中规范的主机测试骨架。compose/material3/material3/src/androidDeviceTest/.../SwitchTest.kt— 规范的设备测试骨架。- 同级技能:
../../concepts/understanding-the-testing-pyramid/SKILL.md、../../concepts/choosing-what-to-test/SKILL.md、../../doubles/picking-test-doubles/SKILL.md、../applying-testing-strategies/SKILL.md。 - 跨类别:
../../../compose/setup/configuring-test-dependencies/SKILL.md、../../../jvm-tests/runner/configuring-junit4-on-android/SKILL.md、../../../jvm-tests/robolectric/using-robolectric-correctly/SKILL.md、../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md、../../../adb/tests/running-instrumented-tests-via-adb/SKILL.md。