using-robolectric-correctly について
Robolectric を使って JVM 上で Android フレームワーク依存のテストを実行する、Android 開発者向けのガイドです。ランナー選択、SDK 設定、Shadow、Looper 制御、ホストとデバイス間のテスト共有を扱います。
主な機能
- 01AndroidJUnit4 と Robolectric のランナー選択
- 02@Config による SDK バージョン設定
- 03Shadow による Android システムクラスの代替
- 04一時停止中の Looper キューを処理
- 05ホストとデバイスのテストソースを構成
- 06Android リソース読み込みの有効化
活用シーン
- 01JVM 上で Android フレームワークのコードをテスト
- 02未モックメソッドやリソース不足による失敗を修正
- 03テスト内の Handler と Looper の処理を制御
- 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 | 開始されたサービス/ブロードキャスト/付与された権限を確認します。 |
ShadowLog | android.util.Log の出力をキャプチャします(ShadowLog.stream = System.out)。 |
ShadowSystemClock | SystemClock.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」というエラーで失敗します。
最新の代替手段(推奨順):
- AndroidX スタイルの KMP ソースセット —
androidXMultiplatformプラグインを使用して、commonTestから{androidHostTest, androidDeviceTest}に連携します。Compose も内部でこの方式に移行しています。sourceSets { androidCommonTest { dependsOn(commonTest) } androidDeviceTest { dependsOn(androidCommonTest) } // instrumented APK androidHostTest { dependsOn(androidCommonTest) } // Robolectric on JVM } - 別の Gradle モジュールを作成し、
testとandroidTestの両方からtestImplementation/androidTestImplementationとして依存させます。 - シンボリックリンクまたはコピーを設定時に作成します(最も推奨されない方法です。壊れやすいためです)。
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) }
}
パターン:ParameterizedRobolectricTestRunner — マトリックスが目的の場合
// 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— 同じテストソースをデバイスでも実行する必要がある場合。