mcpField
U
using-robolectric-correctly

using-robolectric-correctly

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

using-robolectric-correctly について

Robolectric を使って JVM 上で Android フレームワーク依存のテストを実行する、Android 開発者向けのガイドです。ランナー選択、SDK 設定、Shadow、Looper 制御、ホストとデバイス間のテスト共有を扱います。

主な機能

  1. 01AndroidJUnit4 と Robolectric のランナー選択
  2. 02@Config による SDK バージョン設定
  3. 03Shadow による Android システムクラスの代替
  4. 04一時停止中の Looper キューを処理
  5. 05ホストとデバイスのテストソースを構成
  6. 06Android リソース読み込みの有効化

活用シーン

  1. 01JVM 上で Android フレームワークのコードをテスト
  2. 02未モックメソッドやリソース不足による失敗を修正
  3. 03テスト内の Handler と Looper の処理を制御
  4. 04JVM とデバイスで共通のテストロジックを実行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/skydoves/android-testing-skills --skill using-robolectric-correctly

インストール後、Claude Code で/using-robolectric-correctlyを入力して読み込まれたか確認します。

詳細

Robolectric を正しく使う — Android を JVM 上で、落とし穴なく動かす

Robolectric は、システムクラスを純粋な Java で再実装したもの(「シャドウ」)に差し替えて、JVM 上で Android をシミュレートします。高速(エミュレーターと比べて 10〜100 倍)ですが、エミュレーターではありません。実際の RenderThread も、実際の Binder も、実際の GPU もありません。このスキルでは、ランナーの選択、@Config の組み合わせ、Looper のキューを処理する手順、AGP 7.2 以降の sharedTest の落とし穴を整理します。Compose と Robolectric 固有の内容については、../../../compose/synchronization/controlling-the-test-clock/SKILL.md を参照してください。

このスキルを使う場面

  • テスト対象のクラスが android.*(Context、Resources、View、PackageManager、Looper)をインポートしており、開発者がエミュレーターで数分かけるのではなく、数秒で JVM 上のテストを実行したい場合。
  • 単体テストが RuntimeException: Method ... not mocked(素の Android jar のデフォルト動作)で失敗し、開発者が Robolectric のシャドウを必要としている場合。
  • 単体テストが Resources$NotFoundException で失敗する場合 — testOptions.unitTests.includeAndroidResources が設定されていません。
  • 開発者が「AndroidJUnit4 と RobolectricTestRunner のどちらを使うべきですか?」と尋ねた場合。
  • Robolectric のデフォルトの LooperMode.PAUSED では、Handler.postDelayed/Looper のキューが進まない場合。
  • 開発者が、同じテストソースを JVM とデバイスの両方で実行する方法(sharedTest/androidHostTest+androidDeviceTest)を尋ねた場合。
  • AGP 7.2 以降で、従来の sharedTest の srcDir 共有パターンが原因となり、"Source directory ... already added to source set" というビルドエラーが発生した場合。

このスキルを使わない場面

  • テスト対象のクラスが android.* をまったくインポートしていない場合。Robolectric は不要なクラス読み込みのオーバーヘッドを 1〜3 秒発生させます — 通常の JUnit4 を使ってください。../../runner/configuring-junit4-on-android/SKILL.md を参照してください。
  • テストでピクセルの正確性、リップルアニメーション、スクリーンショットの差分、アクセシビリティサービス、または RenderThread のタイミングを検証する場合。エミュレーター/デバイスで実行してください。../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md を参照してください。
  • テストがコルーチンと runTest に関するもので、Android フレームワークを必要としない場合。../../coroutines/testing-coroutines-with-runtest/SKILL.md を使ってください。
  • テストが Compose UI のレンダリングに関する場合。ホストテスト環境は ../../../compose/setup/configuring-test-dependencies/SKILL.md で設定し、クロックのセマンティクスについては ../../../compose/synchronization/controlling-the-test-clock/SKILL.md を参照してください。

前提条件

testImplementation("junit:junit:4.13.2")
testImplementation("org.robolectric:robolectric:4.x")
testImplementation("androidx.test:core:1.7.0")              // ApplicationProvider works on both runtimes
testImplementation("androidx.test.ext:junit:1.3.0")         // androidx.test.ext.junit.runners.AndroidJUnit4

(docs/CORPUS.md §G.1;R7。)

android {
    testOptions {
        unitTests {
            includeAndroidResources = true                  // non-negotiable
        }
    }
}

いずれかのテスト(またはテストによって読み込まれるクラス)が R.*、Resources、レイアウト、テーマ、または res/ 内の何かにアクセスする場合は、includeAndroidResources が必要です。これがないと、テスト開始時にリソースの検索で例外がスローされます(R7)。

Robolectric 4.x は、SDK 21 から最新の安定版までをサポートしています。androidx Compose のホストテストでは下限を 23 に固定しています — これを単一の信頼できる情報源として引用してください。

// androidx/compose/ui/ui-test/src/androidHostTest/.../Constants.kt
internal const val RobolectricMinSdk = 23

(R7。)

ランナーの選択 — AndroidJUnit4 と RobolectricTestRunner

どちらのランナーも使用でき、@Config を同じように読み取ります。

ランナー使用場面
androidx.test.ext.junit.runners.AndroidJUnit4デフォルト。 JVM では Robolectric に、デバイスでは実際の AndroidJUnit4ClassRunner に処理を委譲するルーターです。sharedTest パターンの要です。
org.robolectric.RobolectricTestRunnerテストをデバイスで実行してはならない場合に限ります — たとえば、shadowOf(Looper.getMainLooper()).idle() のような Robolectric 固有のシャドウ API を直接使う場合や、ParameterizedRobolectricTestRunner のような Robolectric のサブクラスが必要な場合です。

(R7;docs/CORPUS.md §G.6。)

import androidx.test.ext.junit.runners.AndroidJUnit4
@RunWith(AndroidJUnit4::class)
@Config(minSdk = 23)
class FooTest { /* runs under Robolectric on JVM, real runner on device */ }

import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [33])
class LooperBoundTest { /* uses ShadowLooper APIs directly — JVM only */ }

重要:androidx.test.runner.AndroidJUnit4(ext.junit.runners ではなく runner パッケージ)は @Deprecated です(docs/CORPUS.md §G.2)。必ず androidx.test.ext.junit.runners からインポートしてください。

@Config — SDK、修飾子、アプリケーション

@Config は、クラスまたはメソッドごとにシミュレートされた Android 環境を設定します。メソッドレベルの設定はクラスレベルの設定を上書きします。サブクラスは設定を継承し、上書きできます。

// Single SDK
@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
class FooTest

// SDK matrix — runs once per listed SDK
@Config(sdk = [21, 28, 33])
class BarTest

// Range — runs once at the project's targetSdk if it is >= minSdk
@Config(minSdk = 23)
class BazTest

// Per-method override
class QuxTest {
    @Test @Config(sdk = [Build.VERSION_CODES.TIRAMISU])
    fun onTiramisuOnly() { /* ... */ }

    @Test @Config(qualifiers = "fr-rFR-w820dp")           // locale + screen
    fun frenchLayout() { /* ... */ }

    @Test @Config(qualifiers = "+night")                   // additive: keep, force night
    fun darkColors() { /* ... */ }
}

// Override the Application instance
@Config(application = MyTestApplication::class)
class WithFakeApp

(R7。)

制約:同じアノテーションで sdk と minSdk/maxSdk を併用することはできません。minSdk と maxSdk は組み合わせて使用できます。

CI の実行時に関する見解:コミットゲートでは @Config(sdk = [<one SDK>]) を優先し、マージゲートでは複数 SDK のマトリックスを使用します。マトリックス内の SDK ごとにクラス全体が再実行されます。

シャドウ

シャドウは、Android のシステムクラスを JVM で扱えるようにするフェイクです。Robolectric には何百ものシャドウが用意されています。@Implements/@Implementation を使って独自のシャドウを作成することもできます。一般的なものは次のとおりです。

シャドウ用途
ShadowApplication開始されたサービス/ブロードキャスト/付与された権限を確認します。
ShadowLogandroid.util.Log の出力をキャプチャします(ShadowLog.stream = System.out)。
ShadowSystemClockSystemClock.elapsedRealtime()/uptimeMillis() を進めたり、停止させたりします。
ShadowLooperメッセージキューを実行します。idleMainLooper()、runUiThreadTasksIncludingDelayedTasks() を使用します。
ShadowPackageManagerパッケージを追加/削除したり、システム機能やクエリ可能なインテントを設定したりします。
ShadowAlarmManagerスケジュールされたアラームを発火させずに確認します。
ShadowNotificationManager投稿された通知とチャンネルを確認します。
ShadowInputManager入力テスト用に InputDevice を追加/削除します。
ShadowContentResolverフェイクプロバイダーを登録し、挿入/クエリを監視します。

Shadows.shadowOf(realObject) はシャドウのインスタンスを返します。静的インポートを使用し、オーバーロード解決に任せます。

import androidx.test.core.app.ApplicationProvider
import org.robolectric.Shadows.shadowOf
import org.robolectric.shadows.ShadowPackageManager

val app: Application = ApplicationProvider.getApplicationContext()
val shadowPm: ShadowPackageManager = shadowOf(app.packageManager)
shadowPm.setSystemFeature(PackageManager.FEATURE_CAMERA_ANY, true)

Looper の処理 — 最も見落とされやすい手順

Robolectric のメイン Looper は、デフォルトのランタイムモード LooperMode.PAUSED では一時停止されています。SUT が投稿した Handler メッセージは自動では実行されません。明示的に処理してください。

import android.os.Looper
import org.robolectric.Shadows.shadowOf
import org.robolectric.shadows.ShadowLooper
import java.time.Duration

// Drain everything currently queued on the main looper.
shadowOf(Looper.getMainLooper()).idle()

// Run delayed tasks too (postDelayed et al).
ShadowLooper.runUiThreadTasksIncludingDelayedTasks()

// Advance virtual time by N ms; only handlers due before that time fire.
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(500))

(R7。)

これは Compose の MainTestClock.advanceTimeBy(...) を補完するもので、独立して機能します。Compose のクロックとプラットフォームの Looper は別々です。Compose on Robolectric では、runComposeUiTest { … waitForIdle() } が RobolectricIdlingStrategy を通じて Looper を実行します。Compose の外では、手動で処理してください。

sharedTest/androidHostTest/androidDeviceTest

従来の sharedTest パターン(2021 年の Robolectric ブログ記事に記載)では、1 つのソースディレクトリを test と androidTest の両方に配置します。

// LEGACY — broken on AGP 7.2+
android {
    sourceSets {
        String dir = 'src/sharedTest/'
        test       { java.srcDirs += dir + 'java' }
        androidTest { java.srcDirs += dir + 'java' }
    }
}

重大な注意:AGP 7.2 以降では、同じソースディレクトリを複数のソースセットに追加できません(Robolectric GitHub issue #7432、R7)。ビルドは「Source directory ... already added to source set」というエラーで失敗します。

最新の代替手段(推奨順):

  1. AndroidX スタイルの KMP ソースセット — androidXMultiplatform プラグインを使用して、commonTest から {androidHostTest, androidDeviceTest} に連携します。Compose も内部でこの方式に移行しています。
    sourceSets {
        androidCommonTest  { dependsOn(commonTest) }
        androidDeviceTest  { dependsOn(androidCommonTest) }   // instrumented APK
        androidHostTest    { dependsOn(androidCommonTest) }   // Robolectric on JVM
    }
    
  2. 別の Gradle モジュールを作成し、test と androidTest の両方から testImplementation/androidTestImplementation として依存させます。
  3. シンボリックリンクまたはコピーを設定時に作成します(最も推奨されない方法です。壊れやすいためです)。

androidHostTest と androidDeviceTest はどちらも、AndroidJUnit4 がルーターであるため、変更せずに @RunWith(AndroidJUnit4::class) を使用できます。ソースセットの設定については ../../../fundamentals/strategies/organizing-test-source-sets/SKILL.md を参照してください。

Robolectric を使うべきでないケース

シナリオ適した実行環境
純粋な Kotlin で、android.* のインポートがないsrc/test/ の通常の JUnit4(Robolectric は不要)
ピクセル単位で正確なレンダリング、スクリーンショットテストエミュレーター/デバイス — RenderThread は実環境では動作しますが、Robolectric では動作しません。
アクセシビリティサービスの動作エミュレーター/デバイス。Compose の enableAccessibilityChecks は Robolectric 上でもバリデーターをインストールし、警告を記録します(Build.FINGERPRINT.lowercase() == "robolectric")。Robolectric はアクセシビリティサービスを忠実に動作させないため、テストに合格しても結論は出せません(b/332778271)。
Choreographer の忠実度、本番環境に近いアニメーションのタイミングエミュレーター/デバイス
GPU/Skia の動作エミュレーター/デバイス。@GraphicsMode(GraphicsMode.Mode.NATIVE) のオプトインは、実際のパイプラインのスタブです。

パターン

パターン:誤り — デバイスでも実行すべきテストに RobolectricTestRunner を使う
// WRONG
@RunWith(RobolectricTestRunner::class)
@Config(minSdk = 23)
class LoginViewModelTest {
    @Test fun loginFlow() { /* ... */ }
}
// WRONG because: this class CAN run unchanged on a device too — there are no
// Robolectric-only APIs in the body. Pinning to RobolectricTestRunner locks the
// test to the JVM and forfeits the sharedTest pattern. The same class moved to
// src/androidTest/ (or androidDeviceTest) would not run.
// RIGHT
@RunWith(AndroidJUnit4::class)
@Config(minSdk = RobolectricMinSdk)                      // pin via single source of truth
class LoginViewModelTest {
    @Test fun loginFlow() { /* ... */ }
}
パターン:誤り — looper を処理する前にアサーションする
// WRONG
@RunWith(AndroidJUnit4::class)
class HandlerTest {
    @Test fun postedRuns() {
        var fired = false
        Handler(Looper.getMainLooper()).post { fired = true }
        assertTrue(fired)                                 // FAIL: runnable is queued, not run
    }
}
// WRONG because: Robolectric's default LooperMode.PAUSED does not auto-run posted
// runnables. The post is queued; assertTrue runs before the queue is drained.
// RIGHT
@RunWith(AndroidJUnit4::class)
class HandlerTest {
    @Test fun postedRuns() {
        var fired = false
        Handler(Looper.getMainLooper()).post { fired = true }
        shadowOf(Looper.getMainLooper()).idle()           // drain queue
        assertTrue(fired)
    }
}
パターン:誤り — includeAndroidResources の設定がない
// WRONG
android {
    testOptions {
        unitTests { /* includeAndroidResources missing */ }
    }
}

症状:テストの起動時に android.content.res.Resources$NotFoundException: String resource ID #0x7f0c0001 が発生します。修正方法:

// RIGHT
android {
    testOptions {
        unitTests {
            includeAndroidResources = true                // Groovy
            // isIncludeAndroidResources = true           // Kotlin DSL
        }
    }
}
パターン:誤り — AGP 7.2 以降で sharedTest srcDir を共有する
// WRONG
android {
    sourceSets {
        test       { java.srcDirs += 'src/sharedTest/java' }
        androidTest { java.srcDirs += 'src/sharedTest/java' }
    }
}
// WRONG because: AGP 7.2+ rejects sharing a single source directory across
// source sets (Robolectric issue #7432). Build fails with "Source directory
// already added to source set".
// RIGHT — AndroidX-style KMP source sets
sourceSets {
    androidCommonTest  { dependsOn(commonTest) }
    androidDeviceTest  { dependsOn(androidCommonTest) }
    androidHostTest    { dependsOn(androidCommonTest) }
}
パターン:Parameter​​izedRobolectricTestRunner — マトリックスが目的の場合
// androidx/compose/ui/ui-test/.../ViewVisibilityRobolectricTest.kt (paraphrased)
@RunWith(ParameterizedRobolectricTestRunner::class)
@Config(minSdk = RobolectricMinSdk)
class ViewVisibilityRobolectricTest(private val visibility: Int) {
    companion object {
        @JvmStatic
        @Parameters(name = "visibility={0}")
        fun params() = listOf(View.VISIBLE, View.INVISIBLE, View.GONE)
    }
    @Test fun behaves() { /* uses `visibility` */ }
}

RobolectricTestRunner(そのパラメーター化サブクラス)を指定するのはここだけです。AndroidJUnit4 にはパラメーター化されたバリアントがありません。

パターン:Robolectric に応じて本番コードを分岐させる — 注意して読む
// androidx/compose/ui/ui-test/.../RobolectricIdlingStrategy.android.kt
internal val HasRobolectricFingerprint
    get() = Build.FINGERPRINT.lowercase() == "robolectric"

この慣用句は androidx 内で広く使われています(Compose のアクセシビリティ、アイドリング、グラフィックスレイヤー)。テスト対象のライブラリコードでは、分岐が本当にテスト専用でない限り、これをコピーしないでください。誤検出すると、動作が気付かないうちに変わります。

必須ルール

  • 必須: androidx.test.ext.junit.runners から AndroidJUnit4 をインポートしてください。androidx.test.runner からのインポートは禁止です。こちらは @Deprecated です(docs/CORPUS.md §G.2)。
  • 必須: @RunWith(RobolectricTestRunner::class) よりも @RunWith(AndroidJUnit4::class) を優先してください。後者を使うのは、テストで Robolectric 固有の API(例:shadowOf(Looper.getMainLooper()).idle())を直接使用する場合、または ParameterizedRobolectricTestRunner が必要な場合に限ります。
  • 必須: 単体テストで R.*/Resources/レイアウト/テーマに触れるプロジェクトでは、testOptions.unitTests.includeAndroidResources = true を設定してください。
  • 必須: Handler.post* コールバックに依存するものをアサーションする前に、shadowOf(Looper.getMainLooper()).idle()(または ShadowLooper.runUiThreadTasksIncludingDelayedTasks())で looper を処理してください。デフォルトの LooperMode.PAUSED では自動実行されません。
  • 必須: クラスレベルの @Config(minSdk = …)(または sdk = […])を指定してください。CI でプロジェクトのデフォルト SDK が変わることに依存しないでください。
  • 禁止: AGP 7.2 以降で、レガシーな srcDirs 共有パターン test.java.srcDirs += 'src/sharedTest/java' を使わないでください。androidHostTest と androidDeviceTest(KMP ソースセット)に移行するか、別の Gradle モジュールにしてください。
  • 禁止: android.* のインポートが一つもないテストクラスに Robolectric を追加しないでください。クラスのロードに 1~3 秒かかるだけで、何の利点もありません。
  • 禁止: スクリーンショットテスト、リップルアニメーション、アクセシビリティサービス、RenderThread のタイミングに Robolectric を使わないでください。エミュレーター/デバイスを使ってください。
  • 推奨: androidHostTest モジュールの下限 SDK の単一の信頼できる情報源として、androidx/compose/ui/ui-test/src/androidHostTest/.../Constants.kt の RobolectricMinSdk = 23 を参照してください。
  • 推奨: クラスごとのクラスロードコストを分散できるよう、関連するテストを同じクラスにまとめてください。多数の小さなクラスに @RunWith(RobolectricTestRunner::class) を散在させないでください。
  • 推奨: コミットゲートでは単一 SDK の @Config(sdk = [33]) を使い、マージゲートでは複数 SDK のマトリックスを使ってください。

検証

  • androidx.test.runner.AndroidJUnit4 をインポートしていないこと(androidx.test.ext.junit.runners.AndroidJUnit4 を使用してください)。grep -rn 'androidx.test.runner.AndroidJUnit4' src/test src/androidTest の結果が空であること。
  • モジュールの build.gradle(.kts) で testOptions.unitTests.includeAndroidResources = true が設定されていること。
  • Robolectric 固有の API を実際には使用しないテストクラスに @RunWith(RobolectricTestRunner::class) が存在しないこと。
  • Handler に投稿する、または Looper の処理を発生させるすべてのテストで、アサーションの前に shadowOf(Looper.getMainLooper()).idle()(または runUiThreadTasksIncludingDelayedTasks)を呼び出していること。
  • すべての Robolectric テストクラスで @Config(minSdk = …) または @Config(sdk = […]) を宣言していること。
  • ソースセットの構成で、レガシーな srcDirs += sharedTest ではなく、androidHostTest + androidDeviceTest(または共有モジュール)を使用していること。
  • スクリーンショット/リップル/アクセシビリティサービスのテストを Robolectric で実行していないこと。これらのテストは src/androidTest/ または androidDeviceTest に配置してください。
  • ./gradlew :module:test が成功すること。androidCommonTest に含まれるテストについては、./gradlew :module:connectedDebugAndroidTest も成功すること。

参考資料

  • Robolectric — はじめに:http://robolectric.org/getting-started/
  • Robolectric — 設定:http://robolectric.org/configuring/
  • Robolectric — sharedTest のブログ記事:http://robolectric.org/blog/2021/10/06/sharedTest/
  • Android Developers — ローカル単体テストのビルド:https://developer.android.com/training/testing/local-tests
  • Android Developers — Robolectric:https://developer.android.com/training/testing/local-tests/robolectric
  • Robolectric GitHub issue #7432 — AGP 7.2 による sharedTest の破損:https://github.com/robolectric/robolectric/issues/7432
  • 調査資料:tasks/research/R7-robolectric.md。
  • docs/CORPUS.md §G.1 / §G.6 — Gradle の依存関係とランナーの選択。
  • androidx/compose/ui/ui-test/src/androidHostTest/.../Constants.kt — RobolectricMinSdk = 23 の下限。
  • androidx/compose/ui/ui-test/.../RobolectricComposeTest.kt — クロスランタイムテストの標準例:@RunWith(AndroidJUnit4::class) @Config(minSdk = RobolectricMinSdk)。
  • androidx/compose/ui/ui-test/.../PrefetchNotHangingMainThreadTest.kt — @RunWith(RobolectricTestRunner::class) と、明示的な shadowOf(Looper.getMainLooper()).idle() の例。
  • androidx/compose/ui/ui-test/.../ViewVisibilityRobolectricTest.kt — ParameterizedRobolectricTestRunner の例。
  • androidx/compose/ui/ui-test/.../RobolectricIdlingStrategy.android.kt — Build.FINGERPRINT.lowercase() == "robolectric" による検出の慣用表現。
  • androidx/compose/ui/ui/.../MediaQueryIntegrationTest.kt — @RunWith(AndroidJUnit4::class) のテストでの ShadowPackageManager / ShadowInputManager の使用例。
  • 関連資料:../../coroutines/testing-coroutines-with-runtest/SKILL.md — runTest + MainDispatcherRule(Robolectric とは独立)。
  • 関連資料:../../coroutines/testing-flows-with-turbine/SKILL.md — Flow のアサーション。Turbine は Robolectric でも変更なしで動作します。
  • 関連資料:../../runner/configuring-junit4-on-android/SKILL.md — Robolectric を取り巻く JUnit4 の設定。
  • 関連資料:../../mocking/mocking-with-mockito/SKILL.md — Robolectric での Android 型のモック。
  • 関連資料:../../mocking/mocking-with-mockk/SKILL.md — フィンガープリントの分岐に対する mockkStatic(Build::class)。
  • セット横断:../../../fundamentals/strategies/applying-testing-strategies/SKILL.md — Google の 3 段階の規模分類では、Robolectric は「中規模」テストです。
  • セット横断:../../../fundamentals/strategies/organizing-test-source-sets/SKILL.md — src/test/ と src/androidTest/、androidHostTest/androidDeviceTest の使い分け。
  • セット横断:../../../compose/synchronization/controlling-the-test-clock/SKILL.md — Compose の MainTestClock は ShadowLooper とは別のものです。
  • セット横断:../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md — 同じテストソースをデバイスでも実行する必要がある場合。

コメント