mcpField
O
organizing-test-source-sets

organizing-test-source-sets

github.com/skydovesテスト・品質33964 回インストール22 週間前に更新
リポジトリ

organizing-test-source-sets について

Android 開発者向けに、テストの配置先や Gradle の依存関係設定、旧 sharedTest 構成からの移行を説明するテストスキルです。ローカル JVM、計装テスト、ホスト/デバイスのソースセットを区別します。

主な機能

  1. 01src/test/ と src/androidTest/ の使い分け
  2. 02testImplementation と androidTestImplementation の説明
  3. 03テストマニフェスト向け debugImplementation の解説
  4. 04sharedTest の移行方法
  5. 05androidHostTest と androidDeviceTest の説明

活用シーン

  1. 01Robolectric テストの配置先選び
  2. 02誤った Gradle テスト依存関係の修正
  3. 03AGP 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 の分割に移行しました。このスキルでは、エージェントが自信を持ってテストファイルを配置できるよう、この対応関係をまとめています。

このスキルを使用する場合

  • ユーザーが「テストはどこに置けばよいですか?」または「src/test/ と src/androidTest/ のどちらですか?」と尋ねた場合。
  • ユーザーが「testImplementation と androidTestImplementation と debugImplementation の違いは何ですか?」と尋ねた場合。
  • ユーザーから Unresolved reference: createComposeRule の報告があり、エージェントが依存関係の設定先が誤っていると考えた場合。
  • AGP 7.2 以降で sourceSets { test.java.srcDir(...) } ブロックが動作しなくなり、ユーザーが src/sharedTest/ から移行しようとしている場合。
  • ユーザーが androidHostTest / androidDeviceTest に言及し、非 KMP モジュールでの設定方法を尋ねた場合。
  • ユーザーが「src/androidTest/ にある Robolectric テストはなぜですか?」と尋ねた場合 — src/test/ に置くべきです。

このスキルを使用しない場合

  • ユーザーが Compose テスト(ui-test-junit4、ui-test-manifest)の Gradle 依存関係を設定している場合 — ../../../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 が文書化している 2 つのソースセット

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)

エージェントが区別しておくべき3つの構成です。

  • testImplementation — JVM(ホスト)テスト用です。Mockito、MockK、Robolectric、kotlinx-coroutines-test、Turbineが含まれます。
  • androidTestImplementation — インストルメンテーション(デバイス)テスト用です。Espresso、UiAutomator、AndroidJUnitRunner、Compose ui-test-junit4が含まれます。
  • debugImplementation — メインのdebugバリアント用です。テスト APK は debug APK をベースにビルドされ、マニフェストエントリをマージする必要があるため、androidx.compose.ui:ui-test-manifestとandroidx.fragment:fragment-testing-manifestで使用されます。

androidx.compose.ui:ui-test-manifestは、androidTestImplementationではなく、必ずdebugImplementationに指定してください。これを強制するLintチェック(TestManifestGradleConfiguration)については、../../../compose/setup/configuring-test-dependencies/SKILL.mdを参照してください。

src/sharedTest/ — コミュニティの慣例であり、Googleのドキュメントには記載なし

tasks/research/R8-android-fundamentals.mdによると:

src/sharedTest/は、対象となる6つのdeveloper.android.com/training/testing/...ページにも、取得済みのHiltテスト/rulesページにも、名前を挙げて記載されていません。

このパターンは、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バージョン表があります)。多くのモジュールでは、次のいずれかの方法を採用しています。

  • テストの共有をやめ、重複を受け入れました。
  • テストヘルパーをtestImplementationとandroidTestImplementation経由で提供する、別のGradleサブモジュール(:core-testing)に移行しました。
  • 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 } })を使う方法です。これにより、testFixturesImplementation用のtestFixtures() Gradleアーティファクトが公開され、兄弟モジュールのtestImplementation/androidTestImplementationから利用できます。

sharedTestについてdeveloper.android.com/training/testing/...を引用してはいけません。該当ページにはその記載がありません。代わりに、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")
}
パターン:誤りと正しい例 — src/androidTest/内のRobolectric
// 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 には決して指定しないでください。
  • 禁止: src/sharedTest/ の根拠として developer.android.com/training/testing/... を引用しないでください。そこには記載されていません。Architecture Blueprints リポジトリまたは tasks/research/R8-android-fundamentals.md を引用してください。
  • 禁止: Espresso、UiAutomator、または ui-test-junit4 を testImplementation に追加しないでください。コンパイルはできますが、Instrumentation がない場合は実行時に失敗します。
  • 推奨: テストヘルパーを共有する場合は、AGP 7.2 で問題が起きやすい sharedTest ソースセットではなく、:core-testing Gradle サブモジュールを切り出すか、android.testFixtures.enable = true を使用してください。
  • 推奨: 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 — 「大規模なローカルテスト:Robolectric など、ローカルで実行される Android シミュレーターを使用できます」。
  • tasks/research/R8-android-fundamentals.md — 引用文そのまま。src/sharedTest/ が対象範囲内の /training/testing/... の 6 ページのいずれにも記載されていないことを記録しています。
  • 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。

コメント