Organizing Test Source Sets — src/test/, src/androidTest/, and the KMP Split
Where a test file lives determines which JVM runs it, which dependencies it sees, and whether it ships in the test APK. Most modules need only src/test/ and src/androidTest/. Modules that share helpers between host and device tests historically used src/sharedTest/ (a Codelab convention, not Google-documented as such), which broke on AGP 7.2+ and pushed Compose itself to the KMP-style androidHostTest / androidDeviceTest split. This skill encodes the matrix so the agent can place a test file with confidence.
When to use this skill
- The user asks "where do I put my tests" or "src/test/ or src/androidTest/".
- The user asks "what's testImplementation vs androidTestImplementation vs debugImplementation".
- The user reports
Unresolved reference: createComposeRuleand the agent suspects the dependency is on the wrong configuration. - The user is migrating off
src/sharedTest/after AGP 7.2+ broke theirsourceSets { test.java.srcDir(...) }block. - The user mentions
androidHostTest/androidDeviceTestand asks how to set them up in a non-KMP module. - The user asks "why is my Robolectric test in
src/androidTest/" — it should be insrc/test/.
When NOT to use this skill
- The user is wiring Gradle dependencies for Compose test (
ui-test-junit4,ui-test-manifest) — use../../../compose/setup/configuring-test-dependencies/SKILL.md. - The user is choosing test scope (small/medium/big) — use
../../concepts/understanding-the-testing-pyramid/SKILL.md. - The user is configuring
AndroidJUnitRunneritself — use../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md. - The user is configuring a JUnit4 runner for JVM tests — use
../../../jvm-tests/runner/configuring-junit4-on-android/SKILL.md.
Prerequisites
- A module with the Android Gradle Plugin (
com.android.applicationorcom.android.library). - Familiarity with Gradle configuration names (
testImplementation,androidTestImplementation,debugImplementation). - An understanding that JVM tests run on the host's JVM and instrumented tests run on a device/emulator. (See
../../concepts/understanding-the-testing-pyramid/SKILL.md.)
The two source sets Google documents
src/test/ — local JVM tests
Per /training/testing/local-tests:
"By default, the source files for local unit tests are placed in
module-name/src/test/. This directory already exists when you create a new project using Android Studio." —developer.android.com/training/testing/local-tests
"A local test runs directly on your own workstation, rather than an Android device or emulator. As such, it uses your local Java Virtual Machine (JVM)." —
developer.android.com/training/testing/local-tests
Driven by testImplementation. Runs via ./gradlew test or ./gradlew testDebugUnitTest. No Android device, no emulator. The Android framework is unavailable unless Robolectric simulates it.
src/androidTest/ — instrumented tests
Per /training/testing/instrumented-tests:
"In your Android Studio project, you store the source files for instrumented tests in
module-name/src/androidTest/java/. This directory already exists when you create a new project and contains an example instrumented test." —developer.android.com/training/testing/instrumented-tests
Driven by androidTestImplementation. Runs via ./gradlew connectedAndroidTest or ./gradlew connectedDebugAndroidTest. Requires a connected device or emulator. Uses AndroidJUnitRunner per CORPUS §H.2.
The page is also explicit on the testImplementation vs androidTestImplementation split:
"Note:
testImplementationadds dependencies for local tests andandroidTestImplementationadds dependencies for Instrumented tests." —developer.android.com/training/testing/local-tests
The configuration matrix
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)
Three configurations the agent must keep straight:
testImplementation— JVM (host) tests. Includes Mockito, MockK, Robolectric, kotlinx-coroutines-test, Turbine.androidTestImplementation— instrumented (device) tests. Includes Espresso, UiAutomator, AndroidJUnitRunner, Composeui-test-junit4.debugImplementation— maindebugvariant. Used byandroidx.compose.ui:ui-test-manifestandandroidx.fragment:fragment-testing-manifestbecause the test APK is built on top of the debug APK and needs the manifest entries merged in.
androidx.compose.ui:ui-test-manifest MUST go on debugImplementation, not androidTestImplementation — see ../../../compose/setup/configuring-test-dependencies/SKILL.md for the lint check (TestManifestGradleConfiguration) that enforces this.
src/sharedTest/ — community convention, NOT Google-documented
Per tasks/research/R8-android-fundamentals.md:
src/sharedTest/is not mentioned by name on any of the six in-scopedeveloper.android.com/training/testing/...pages or on the fetched Hilt-testing / rules pages.
It originated in Google's own Architecture Blueprints sample and the Advanced Android Testing Codelab. The pattern uses Gradle sourceSets to inject the same Java/Kotlin folder into BOTH test and 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")
}
}
Why it existed: write a test once, run it as a fast Robolectric host test in CI and as a high-fidelity instrumented test on emulator. The test class uses androidx.test.ext.junit.runners.AndroidJUnit4 (which dispatches correctly in both environments per CORPUS §G.6).
Why it broke: AGP 7.2+ enforces that the same source directory cannot be shared across multiple source sets at build time — the build itself refuses, with an error pointing at the duplicated srcDir(...) call. (The mode of failure is build-time refusal in AGP, not a runtime LinkageError.) Tracked at https://github.com/robolectric/robolectric/issues/7432 (Robolectric maintainer thread, with reproductions and AGP-version table). Many modules either:
- Stopped sharing tests and accepted duplication.
- Migrated to a separate Gradle sub-module (
:core-testing) that ships test helpers viatestImplementationandandroidTestImplementation. - Adopted KMP-style source set names (
androidHostTest/androidDeviceTest).
KMP-style androidHostTest / androidDeviceTest
Compose UI tests internally use this layout, per CORPUS §C ("Source set conventions (KMP)"):
commonTest
└── androidCommonTest (helpers shared by host + device)
├── androidDeviceTest (instrumentation APK on emulator/device)
└── androidHostTest (Robolectric on JVM)
Real androidx evidence: compose/ui/ui-test/src/androidHostTest/kotlin/androidx/compose/ui/test/RobolectricComposeTest.kt is a host (Robolectric) test, while compose/material3/material3/src/androidDeviceTest/.../SwitchTest.kt is a device test. Both compile against commonTest for shared helpers.
For non-KMP Android-only modules, the same effect can be approximated with separate sub-modules:
// :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
The :feature:home-testing module hosts FakeUserRepository, MainDispatcherRule, custom matchers — anything both host and device tests want.
Alternative: AGP 7.0+ testFixtures flag (android { testFixtures { enable = true } }), which exposes testFixtures() Gradle artifacts for testFixturesImplementation and is consumable from sibling modules' testImplementation / androidTestImplementation.
MUST NOT cite developer.android.com/training/testing/... for sharedTest — none of those pages document it. MUST cite the Architecture Blueprints repo or tasks/research/R8-android-fundamentals.md instead.
File placement decision tree
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)
Patterns
Pattern: WRONG vs RIGHT — testImplementation vs 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")
}
Pattern: WRONG vs RIGHT — Robolectric in 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 { /* ... */ }
Pattern: WRONG vs RIGHT — ui-test-manifest configuration
// 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")
}
See ../../../compose/setup/configuring-test-dependencies/SKILL.md for the lint detail.
Pattern: AGP 7.2+ sharedTest migration
// 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 itself uses Option C internally (per CORPUS §B "Source set conventions").
Module-level Gradle sample
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")
}
Mandatory rules
- MUST put pure Kotlin / JVM tests in
src/test/withtestImplementationdeps. - MUST put Robolectric tests in
src/test/. Robolectric is a JVM Android simulator, not an instrumented runner. - MUST put real-device / emulator tests in
src/androidTest/withandroidTestImplementationdeps. - MUST put
androidx.compose.ui:ui-test-manifestandandroidx.fragment:fragment-testing-manifestondebugImplementation, neverandroidTestImplementation. - MUST NOT cite
developer.android.com/training/testing/...as the source forsrc/sharedTest/. It is not documented there. Cite the Architecture Blueprints repo ortasks/research/R8-android-fundamentals.md. - MUST NOT add Espresso, UiAutomator, or
ui-test-junit4totestImplementation. They will compile but fail at runtime when noInstrumentationis present. - PREFERRED: for shared test helpers, extract a
:core-testingGradle sub-module or useandroid.testFixtures.enable = truerather than the AGP-7.2-fragilesharedTestsource set. - PREFERRED: in KMP modules, follow Compose's lead —
androidCommonTestfor helpers,androidHostTestfor Robolectric,androidDeviceTestfor emulator tests. - PREFERRED: name backtick test methods freely in
src/test/; restrict to API 30+ devices insrc/androidTest/per/training/testing/instrumented-tests.
Verification
- No Robolectric test exists under
src/androidTest/. Runfind src/androidTest -name '*.kt' -exec grep -l 'org.robolectric' {} +; expect empty output. - No Espresso / UiAutomator import exists under
src/test/. Runfind src/test -name '*.kt' -exec grep -l 'androidx.test.espresso\|androidx.test.uiautomator' {} +; expect empty output. -
androidx.compose.ui:ui-test-manifestis declared exactly ondebugImplementation. Run./gradlew :<module>:lint; expect noTestManifestGradleConfigurationwarning. -
./gradlew :<module>:testDebugUnitTestruns JVM tests without invokingconnectedDebugAndroidTest. -
./gradlew :<module>:connectedDebugAndroidTestinvokes only tests undersrc/androidTest/(verify with--infooutput). - If shared helpers exist, they live in a
:core-testingsub-module, intestFixtures, or inandroidCommonTest— never in a literalsrc/sharedTest/folder wired viasourceSets { ... srcDir(...) }. -
testOptions.unitTests.isIncludeAndroidResources = trueis set when any host test referencesR.*.
References
developer.android.com/training/testing/local-tests—src/test/,testImplementation, "local test runs directly on your own workstation".developer.android.com/training/testing/instrumented-tests—src/androidTest/,androidTestImplementation, AndroidJUnitRunner, backtick caveat.developer.android.com/training/testing/fundamentals— "Big local test: You can use an Android simulator that runs locally, such as Robolectric".tasks/research/R8-android-fundamentals.md— verbatim quotes; documents thatsrc/sharedTest/is NOT on any of the six in-scope/training/testing/...pages.- Architecture Blueprints (origin of the
sharedTestpattern): https://github.com/android/architecture-samples - AGP source-set documentation: https://developer.android.com/build/build-variants#sourcesets
testFixturesflag: https://developer.android.com/build/dependencies#test-fixtures- CORPUS §C "Source set conventions (KMP)" — Compose's internal
androidHostTest/androidDeviceTestlayout. compose/ui/ui-test/src/androidHostTest/kotlin/androidx/compose/ui/test/RobolectricComposeTest.kt— canonical host-test skeleton in androidx.compose/material3/material3/src/androidDeviceTest/.../SwitchTest.kt— canonical device-test skeleton.- Sibling skills:
../../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. - Cross-category:
../../../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.