mcpField
O
organizing-test-source-sets

organizing-test-source-sets

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

关于 organizing-test-source-sets

面向 Android 开发者的测试技能,说明测试文件的放置位置、对应的 Gradle 配置,以及如何替换旧的 sharedTest 布局。区分本地 JVM、仪器测试和主机/设备测试源集。

主要功能

  1. 01区分 src/test/ 与 src/androidTest/
  2. 02说明 testImplementation 和 androidTestImplementation
  3. 03解释测试清单使用 debugImplementation
  4. 04介绍 sharedTest 迁移方案
  5. 05说明 androidHostTest 与 androidDeviceTest

适用场景

  1. 01为 Robolectric 测试选择源集
  2. 02修复测试依赖配置错误
  3. 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、Compose ui-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-testing Gradle 子模块,或使用 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。

评论

支持 Markdown