Error [WorkManager is not initialized properly]

`WorkManager is not initialized properly` の直し方(Robolectric のテストで WorkManager を取り出せない)

FIX SUMMARY verified
Applies when
eclipse-temurin@sha256:89dc1a6e09920ea26b2ede6fddfcac1a7508b50159a6d04c918a46132953aab6WorkManager.getInstance() in a Robolectric test where no startup initializer ran; the fix is WorkManagerTestInitHelper.initializeTestWorkManager() from androidx.work:work-testing, which yields a WorkManager driven by TestScheduler (a TestDriver you can advance)

Verified: reproduced in eclipse-temurin@sha256:89dc1a6e09920ea26b2ede6fddfcac1a7508b50159a6d04c918a46132953aab6, then the WorkManager is not initialized properly signature was gone after the fix (exit 0).

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.gradleandroidx.work:work-testing を足します(版は work-runtime と揃えます)。

testImplementation "androidx.work:work-testing:$work_version"

エラー文が並べているのは「候補」であって、検査結果ではない

このメッセージは 3 つの条件を並べています。

  1. マニフェストで WorkManagerInitializer を明示的に無効にしている
  2. この時点までに WorkManager#initialize を手で呼んでいない
  3. ApplicationConfiguration.Provider を実装していない

ただしこれは、JVM が 3 つを個別に検査した結果ではありません。 WorkManagerImpl.getInstance() が実際に見ているのは、(a)singleton がまだ作られていないか、(b)application context が Configuration.Providerの 2 点だけです。(a) で未初期化、(b) で Provider でもない、というときに、上の 3 行が固定の文字列として投げられます。マニフェストを読み直して「明示的に無効化されているか」を確かめるコードはありません。

つまり**この 3 行は「初期化されていない理由の候補」**で、あなたの環境で実際に起きたことを名指ししているわけではありません。「マニフェストで無効化した覚えがない」なら、それで正しいのです——**探すべきは「無効化した設定」ではなく、「そもそも初期化する経路が通っていない理由」**です。

なお、(b) は本物の分岐です。テストの ApplicationConfiguration.Provider を実装すると、このエラーは出なくなります(実測)。getInstance() はその場で初期化を進めます。

問題は、そこで進む先が本番用の初期化だということです。

テストで欲しいのは、本番の WorkManager ではない

Configuration.Provider を実装して初期化すると、テストの中に本番と同じ WorkManager ができます。実行用のスレッドプールを持ち、制約(ネットワークがある/充電中)を実際に待ち、遅延を実時間で待ちます。テストからは、いつ仕事が走ったのかを決めることができません。

WorkManagerTestInitHelper で初期化すると、返ってくる WorkManagerクラスは本番と同じです。

WM_CLASS=androidx.work.impl.WorkManagerImpl
TEST_DRIVER=androidx.work.testing.TestScheduler

違うのは構成のほうです。引数なしの initializeTestWorkManager() は、仕事を同期実行する executorSynchronousExecutor)と、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-testingtestImplementation に入っていません。work-runtime同じ版を指定してください。
  • テストごとに状態が残るinitializeTestWorkManager() はテストごとに呼びます。@Before に置くのが素直です。
  • setAllConstraintsMet を呼んでも仕事が走らないTestDriverWorkManagerTestInitHelper.getTestDriver(context) から取ります。initializeTestWorkManager() を呼ばずに getTestDriver() を呼ぶと使えません。
  • WorkManager is already initialized になった:逆向きです。その環境では自動初期化が走っています(マージ済みマニフェストの初期化子が動いた)。テスト用の初期化と衝突しているので、マニフェストから WorkManagerInitializer を取り除いて(tools:node="remove")から、テスト用の初期化を呼んでください。
  • 本番のアプリ(テストではない)で同じ例外が出る:初期化する経路が 1 つも通っていません。選択肢は 3 つで、(a)マニフェストでの無効化をやめて自動初期化に戻す、(b)ApplicationConfiguration.Provider を実装する、(c)WorkManager.initialize() を自分で呼ぶ、です。この場合はテスト用ヘルパではなく、本番の初期化を用意します。
  • NoClassDefFoundError など、別の例外に変わった:初期化そのものは進んでいます(この例外の検査は通過しています)。次の段でリソースやクラスパスが足りていません。
  • Robolectric のテストで、Android の別の初期化に依存した処理が落ちる:同じ形です。テストの中では、実行環境が用意していた前提が揃っていないことがあります。テスト用の初期化 API があるライブラリは、それを明示的に呼ぶのが確実です。

検証環境

  • eclipse-temurin@sha256:89dc1a6e...(JDK 17.0.19)+ Maven、Robolectric 4.16.1(SDK 34)、WorkManager work-runtime / work-testing 2.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 を持たないため)。本文でその範囲を超える主張はしていません。

検証(machine-verified)

この修正は eclipse-temurin@sha256:89dc1a6e09920ea26b2ede6fddfcac1a7508b50159a6d04c918a46132953aab6 のバージョン固定コンテナ内で再現し、修正後に WorkManager is not initialized properly のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs jvm/robolectric-workmanager-not-initialized
● reproduce WorkManager is not initialized properly present ✓
● apply fix exit 0
● re-run WorkManager is not initialized properly gone ✓
PASS verified · eclipse-temurin@sha256:89dc1a6e09920ea26b2ede6fddfcac1a7508b50159a6d04c918a46132953aab6 · signature gone

確認したのは上のイメージの中だけです。別の環境で直らなかった、記述が違う、という場合は 報告してください(対象と検証イメージは件名・本文に入ります)。