Robolectric のテストで WorkManager.getInstance(context) を呼ぶと、テストが例外で落ちます。
java.lang.IllegalStateException: WorkManager is not initialized properly. You have explicitly
disabled WorkManagerInitializer in your manifest, have not manually called WorkManager#initialize
at this point, and your Application does not implement Configuration.Provider.
at androidx.work.impl.WorkManagerImpl.getInstance(WorkManagerImpl.java:170)
at androidx.work.WorkManager.getInstance(WorkManager.java:184)
at dev.errfix.jvm.WorkManagerInitializationTest.getsWorkManager(WorkManagerInitializationTest.java:17)
WorkManager.getInstance() は、すでに初期化された WorkManager を返すだけの関数です。実機ではアプリの起動時に初期化されているので、そのまま返ります。テストで落ちているなら、そのテスト環境で初期化が済んでいません。
テストでの直し方は、getInstance() を呼ぶ前に WorkManagerTestInitHelper でテスト用に初期化することです。
import androidx.work.testing.WorkManagerTestInitHelper;
@Test
public void enqueuesWork() {
Context context = RuntimeEnvironment.getApplication();
WorkManagerTestInitHelper.initializeTestWorkManager(context); // 先に呼ぶ
WorkManager workManager = WorkManager.getInstance(context);
// ...
}
build.gradle に androidx.work:work-testing を足します(版は work-runtime と揃えます)。
testImplementation "androidx.work:work-testing:$work_version"
エラー文が並べているのは「候補」であって、検査結果ではない
このメッセージは 3 つの条件を並べています。
- マニフェストで
WorkManagerInitializerを明示的に無効にしている - この時点までに
WorkManager#initializeを手で呼んでいない ApplicationがConfiguration.Providerを実装していない
ただしこれは、JVM が 3 つを個別に検査した結果ではありません。 WorkManagerImpl.getInstance() が実際に見ているのは、(a)singleton がまだ作られていないか、(b)application context が Configuration.Provider かの 2 点だけです。(a) で未初期化、(b) で Provider でもない、というときに、上の 3 行が固定の文字列として投げられます。マニフェストを読み直して「明示的に無効化されているか」を確かめるコードはありません。
つまり**この 3 行は「初期化されていない理由の候補」**で、あなたの環境で実際に起きたことを名指ししているわけではありません。「マニフェストで無効化した覚えがない」なら、それで正しいのです——**探すべきは「無効化した設定」ではなく、「そもそも初期化する経路が通っていない理由」**です。
なお、(b) は本物の分岐です。テストの Application に Configuration.Provider を実装すると、このエラーは出なくなります(実測)。getInstance() はその場で初期化を進めます。
問題は、そこで進む先が本番用の初期化だということです。
テストで欲しいのは、本番の WorkManager ではない
Configuration.Provider を実装して初期化すると、テストの中に本番と同じ WorkManager ができます。実行用のスレッドプールを持ち、制約(ネットワークがある/充電中)を実際に待ち、遅延を実時間で待ちます。テストからは、いつ仕事が走ったのかを決めることができません。
WorkManagerTestInitHelper で初期化すると、返ってくる WorkManager のクラスは本番と同じです。
WM_CLASS=androidx.work.impl.WorkManagerImpl
TEST_DRIVER=androidx.work.testing.TestScheduler
違うのは構成のほうです。引数なしの initializeTestWorkManager() は、仕事を同期実行する executor(SynchronousExecutor)と、TestScheduler を組み込みます。そして WorkManagerTestInitHelper.getTestDriver(context) から、駆動役(TestDriver)を取り出せます。
TestDriver testDriver = WorkManagerTestInitHelper.getTestDriver(context);
testDriver.setAllConstraintsMet(request.getId()); // 制約が満たされたことにする
testDriver.setInitialDelayMet(request.getId()); // 初期遅延が過ぎたことにする
つまり WorkManagerTestInitHelper の値打ちは、例外を消すことではなく、仕事を進めるつまみを手に入れることです。Configuration.Provider でも例外は消えますが、つまみは付いてきません。テストのために本番の Application に手を入れたうえで、結局スケジューリングを待つことになります。
この検証環境で自動初期化が走らない理由
WorkManager は通常、アプリの起動時に自動で初期化されます。入口はマージ済みの AndroidManifest に登録された仕掛け(androidx.startup の初期化子)で、それはビルドシステムが組み立てます。
この記事の検証環境(Android SDK も Android Gradle Plugin も無い、Maven だけのテスト)には、マージ済みのマニフェストがありません。 だから初期化子が登録されず、getInstance() が上の例外になります。
Gradle と Android Gradle Plugin を使う実プロジェクトの Robolectric テストで、この自動初期化がどこまで走るのかは、この検証では決められません(当プロジェクトはその環境を再現していません)。実際、逆向きの症状——テストで WorkManager が既に初期化されていて WorkManager is already initialized になる、状態がテスト間で持ち越される——も報告されています。Robolectric はマニフェストに宣言された ContentProvider を作るので、マニフェストが渡っている環境では初期化子が走りうるからです。
どちらに転んでも、テストを暗黙の自動初期化に依存させないほうが確実です。WorkManager 公式のテスト手順も、WorkManagerTestInitHelper による明示的な初期化を前提にしています。自動初期化が走る環境では、マニフェストで WorkManagerInitializer を取り除いてから(tools:node="remove")テスト用の初期化を呼ぶ形になります。
切り分け(うまくいかないとき)
WorkManagerTestInitHelperが解決できない(importが赤い):androidx.work:work-testingがtestImplementationに入っていません。work-runtimeと同じ版を指定してください。- テストごとに状態が残る:
initializeTestWorkManager()はテストごとに呼びます。@Beforeに置くのが素直です。 setAllConstraintsMetを呼んでも仕事が走らない:TestDriverはWorkManagerTestInitHelper.getTestDriver(context)から取ります。initializeTestWorkManager()を呼ばずにgetTestDriver()を呼ぶと使えません。WorkManager is already initializedになった:逆向きです。その環境では自動初期化が走っています(マージ済みマニフェストの初期化子が動いた)。テスト用の初期化と衝突しているので、マニフェストからWorkManagerInitializerを取り除いて(tools:node="remove")から、テスト用の初期化を呼んでください。- 本番のアプリ(テストではない)で同じ例外が出る:初期化する経路が 1 つも通っていません。選択肢は 3 つで、(a)マニフェストでの無効化をやめて自動初期化に戻す、(b)
ApplicationにConfiguration.Providerを実装する、(c)WorkManager.initialize()を自分で呼ぶ、です。この場合はテスト用ヘルパではなく、本番の初期化を用意します。 NoClassDefFoundErrorなど、別の例外に変わった:初期化そのものは進んでいます(この例外の検査は通過しています)。次の段でリソースやクラスパスが足りていません。- Robolectric のテストで、Android の別の初期化に依存した処理が落ちる:同じ形です。テストの中では、実行環境が用意していた前提が揃っていないことがあります。テスト用の初期化 API があるライブラリは、それを明示的に呼ぶのが確実です。
検証環境
eclipse-temurin@sha256:89dc1a6e...(JDK 17.0.19)+ Maven、Robolectric 4.16.1(SDK 34)、WorkManagerwork-runtime/work-testing2.9.1、JUnit 4.13.2。Android SDK・Android Gradle Plugin・Gradle は使っていません。- 再現:
WorkManager.getInstance(RuntimeEnvironment.getApplication())がIllegalStateException: WorkManager is not initialized properly.で終了コード 1 - 修正:直前に
WorkManagerTestInitHelper.initializeTestWorkManager(context)を呼ぶとTests run: 1, Failures: 0, Errors: 0で終了コード 0
再現から修正までは errfix の検証ハーネスが機械的に確認しています。Configuration.Provider を実装すると WorkManager is not initialized properly が出なくなること、テスト用に初期化した WorkManager が WorkManagerImpl を返しつつ getTestDriver() から TestScheduler を取り出せることも、同じ環境で実測しています。
getInstance() の分岐が実際に見ているもの(singleton の有無と Configuration.Provider か否か)と、例外文が固定の文字列であることは、AndroidX の実装に拠ります。 テスト用の初期化が同期実行の executor を組み込むこと、tools:node="remove" による初期化子の除去手順は、AndroidX の公式ドキュメントに拠ります。Gradle と Android Gradle Plugin を使う実プロジェクトの Robolectric テストで、WorkManager の自動初期化がどう振る舞うかは実測していません(この検証環境はマージ済みの AndroidManifest を持たないため)。本文でその範囲を超える主張はしていません。