Robolectric のテストが、実行の直前に次のエラーで落ちます。
java.lang.UnsupportedOperationException: Failed to create a Robolectric sandbox:
Android SDK 36 requires Java 21 (have Java 17)
Gradle や IDE のログでは Caused by: java.lang.UnsupportedOperationException: Failed to create a Robolectric sandbox: ... の形で見えます。数字違いの Android SDK 34 requires Java 17 (have Java 11) も、後述のとおり同じ仕組みから出るメッセージです。
テストコードは変えていないのに落ちるので、原因がテスト側にあるようには見えません。実際、犯人はテストコードではなく、Robolectric が選んだ Android SDK と、テストを走らせている JVM の組み合わせです。
その前に:Robolectric の版を確認する
SDK 36 を扱えるのは Robolectric 4.16 以降です。 それより古い Robolectric で SDK 36 を要求すると、このエラーではなく次が出ます(4.15.1 で実測)。
java.lang.IllegalArgumentException: API level 36 is not available
こちらが出ているなら、JDK をいくら上げても直りません。先に Robolectric を 4.16 以降へ上げてください。以下は Robolectric が 4.16 以降であることを前提に書きます。
Robolectric が使う SDK は何が決めるのか
Robolectric がどの SDK でテストを走らせるかは、次の順に決まります(Robolectric のドキュメント)。
- テストの
@Config(sdk = ...)(メソッド → クラス → 基底クラスの順に適用される) robolectric.propertiesのsdk=(package 階層を上へ探索し、深い package が優先)- どちらも無ければ、モジュールの
targetSdk
compileSdk はこの決定に関与しません。 compileSdk だけを 36 に上げて targetSdk が 35 のままなら、Robolectric は SDK 35 でテストを続けるので、このエラーは出ません。逆に targetSdk を 36 にしたなら、compileSdk を据え置いていても出ます。両方を同時に上げる運用が多いため compileSdk が犯人に見えますが、見るべきは targetSdk と @Config と robolectric.properties です。
直し方は2つ。どちらを選ぶかは「何を対象にテストしたいか」で決まる
1. テストを Java 21 で走らせる(SDK 36 を対象にテストしたいとき)
Android 16 に対してテストしたいなら、選ぶのはこちらになります。Kotlin/Android のモジュールでは次の形が標準です。
// build.gradle.kts(モジュール)
kotlin {
jvmToolchain(21)
}
Android プラグインを当てたモジュールでは、java { toolchain { ... } } を直接書くと構成によっては通りません。Kotlin/Android では kotlin { jvmToolchain(21) } を使ってください(Java の compile とテスト実行の toolchain もこれで揃います)。
CI では、テストを走らせるジョブの JDK も 21 にします(actions/setup-java の java-version: 21 など)。ただし Gradle 自体を Java 21 で動かすには Gradle 8.5 以降が必要なので、古い wrapper のプロジェクトでは、先に wrapper と AGP の Java 21 対応を確認してください。ここを飛ばすと、Robolectric に到達する前に Gradle の起動で落ちます。
17 から 21 へ上げると、JEP 400 の境界(18)を越えます。 テスト実行の JDK が 18 以降になると、charset を指定していない入出力の既定文字コードが native から UTF-8 に変わります。動くのはテスト実行の toolchain だけで本番の実行環境はそのままですが、テストの中で charset 未指定にファイルを読み書きしている箇所があるなら、出力ファイルの文字コードが JDK 18 以降で変わる を先に見てください。テストの失敗メッセージ側が読めなくなる形は テストの失敗メッセージだけ文字化けする にあります。
2. SDK を 35 以下に固定する(Java 17 のまま動かしたいとき)
JDK をすぐに上げられない事情があるなら、下げるのは Robolectric が使う Android SDK のほうです。
// テストクラス
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [35])
class MyTest { /* ... */ }
プロジェクト全体に効かせるなら robolectric.properties(src/test/resources/)に書きます。
sdk=35
ただしこれは**「Android 16 ではなく Android 15 に対してテストする」という宣言**です。エラーは消えますが、targetSdk を上げた目的(新しい SDK での挙動確認)は果たせません。JDK を上げるまでの一時しのぎとして使い、なぜ 35 に固定したかをコメントに残してください。
なぜ Java のバージョンが Android のテストを止めるのか
Robolectric は、エミュレータや実機を使わずに Android のコードを JVM 上で 動かします。
Robolectric は jar を読んでみて失敗しているのではありません。 API レベルごとに必要な Java のバージョンを、自分のコードの中に表として持っています。テストの開始時にその表を引き、実行中の JVM が足りなければ、サンドボックスを作る前に上記の例外で止めます(DefaultSdkProvider の requiredJavaVersion と verifySupportedSdk)。
Robolectric 4.16.1 が持っている表は次のとおりです(配布 jar のバイトコードから読み出した実測値)。
| Android SDK | 必要な Java |
|---|---|
| 29〜33 | 9 |
| 34 | 17 |
| 35 | 17 |
| 36 | 21 |
(表の全体はもっと古い API まで続きます。読み出した範囲では SDK 23〜26 が Java 8 でした。)
Android SDK 34 requires Java 17 (have Java 11) という別バージョンのメッセージも、この表の 34 の行から出ています。原因も対処も同じ構造です。
要件が定数として焼き込まれている以上、シャドウ jar を差し替えたり自前でビルドし直したりしても、このエラーは消えません。逆に、Robolectric の版を上げれば表そのものが変わる(4.16 で SDK 36 の行が追加された)という、正しい期待にもつながります。
版の境界(実測)
Robolectric 4.16.1 で、SDK と JDK の組み合わせを変えて測った結果です。
| テストを走らせる JVM | @Config(sdk = ...) | 結果 |
|---|---|---|
| Java 17 | 34 | 通る |
| Java 17 | 35 | 通る |
| Java 17 | 36 | Failed to create a Robolectric sandbox |
| Java 21 | 36 | 通る |
要求が Java 21 に上がるのは SDK 36 だけです。35 以下は Java 17 で動き、Java 21 に上げれば 36 も動きます。
同じ文言が「警告」としても出る(ただし切り分けには使えない)
Java 17 で走らせると、テストが通る場合でも失敗する場合でも、次の警告がログに出ます。
[Robolectric] WARN: Android SDK 36 requires Java 21 (have Java 17). Tests won't be run on SDK 36 unless explicitly requested.
Robolectric が使える SDK を起動時に数え上げ、「36 は今の JVM では動かせない」と告げているだけの行です。このエラーで落ちているときのログにも、この WARN は並んで出ます。したがって WARN があること自体は、何の手がかりにもなりません。
見るべきはこちらです。
Failed to create a Robolectric sandbox:が出て、テストがエラーになっている → 本記事の症状。SDK 36 が選ばれている。- WARN は出ているが、テストは成功している(終了コード 0) → SDK 36 は選ばれていない。対処は要りません。 ログを消したいだけなら、放置して構いません。
「同じメッセージで検索したのに、自分のテストは落ちていない」という場合は後者です。
なぜ CI で出るのか(走らせる JVM の版で決まる)
SDK 36 が選ばれている前提で、このエラーが出るかどうかは、テストを走らせる JVM が 21 未満かどうかで決まります。そして、その JVM を決めているのはコードではなく環境です。ローカルは IDE に紐づいた Gradle JDK、CI はワークフローが指定した JDK で走ります。この2つが揃っている保証はどこにもありません。targetSdk を上げた変更をプッシュして初めて CI が赤くなる、という出方をするのはこのためです。
確認するときは、テストを実行している JVM を見てください。./gradlew -version の JVM: 行に出るのは Gradle デーモンを動かしている JVM であって、toolchain を設定していればテストを走らせる JVM はそれとは別になります。toolchain を使っているなら、テストタスクの JVM を確認する必要があります。
切り分け
java.lang.IllegalArgumentException: API level 36 is not available→ Robolectric が古い(4.15 以下)。JDK を上げても直りません。Robolectric を 4.16 以降へ上げてください。- テストは通っていて、ログに
[Robolectric] WARN:が出るだけ → SDK 36 は選ばれていません。対処は不要です。 compileSdkは 36 に上げたのに、このエラーが出ない → 正常です。Robolectric はcompileSdkを見ません。UnsupportedClassVersionError: class file version 65.0→ 別の仕組みです。こちらは Robolectric の事前検査ではなく、JVM がクラスをロードした瞬間に落ちています(新しい JDK でビルドされた成果物を古い JVM が読んだ)。例外に出ているクラス名を見て、どの成果物を誰が読んでいるかを確認してください。詳しくは UnsupportedClassVersionError: class file version 61.0 の直し方 にまとめています。
検証環境
eclipse-temurin:17-jdk(Temurin 17.0.19)、ネットワーク有り。Android SDK・Gradle・AGP は入れていません- Robolectric 4.16.1、JUnit 4.13.2、
org.robolectric:android-all:14-robolectric-10818077、androidx.test:monitor:1.8.0 - 再現:
@Config(sdk = 36)のテストを Maven で実行し、Failed to create a Robolectric sandbox: Android SDK 36 requires Java 21 (have Java 17)が出て終了コード 1 - 修正:同じイメージ・同じ依存のまま
@Config(sdk = 34)に変えて終了コード 0 - 版の境界表の残り2点(Java 17 と SDK 35、Java 21 と SDK 36)、失敗時にも WARN が出ること、Robolectric 4.15.1 では
API level 36 is not availableになること、およびrequiredJavaVersionの表は、いずれも個別に実行して確認しました
build.gradle.kts と robolectric.properties の記述、targetSdk から SDK が決まる規則、Gradle と AGP の Java 21 対応は、Robolectric と Gradle のドキュメントに基づく対処です。機械検証は Maven の最小構成で行っており、Gradle 経由では通していません(Robolectric が JVM の版を検査する箇所は、ビルドツールに依存しません)。