v2 テスト API を使用してテスト環境を構成する

Compose テスト API(createComposeRule、createAndroidComposeRule、runComposeUiTest、runAndroidComposeUiTest など)の v2 バージョンが利用可能になり、コルーチン実行の制御が改善されました。このアップデートでは、API サーフェス全体が複製されるわけではありません。テスト環境を確立する API のみが更新されています。

v1 API は非推奨であり、新しい API に移行することを強くおすすめします。移行することで、テストが標準のコルーチンの動作に沿っていることを確認し、将来の互換性の問題を回避できます。非推奨の v1 API の一覧については、API マッピングをご覧ください。

v1 API は UnconfinedTestDispatcher に依存していましたが、v2 API では、実行中のコンポジションにデフォルトで StandardTestDispatcher が使用されます。この変更により、Compose テストの動作が標準の runTest API と整合し、コルーチンの実行順序を明示的に制御できるようになります。

テスト環境を構成する

Compose テスト v2 API は、ComposeUiTestConfig を使用してテスト環境をカスタマイズします。テストのセットアップ関数を作成する API(createComposeRule、runComposeUiTest、その他の関連 API など)は、ComposeUiTestConfig を受け入れます。この構成オブジェクトは、effectContext、runTestContext、testTimeout などの環境関連の API を 1 つのオブジェクトに統合します。

構成モデルは inputMode も管理します。Compose テスト v2 API では、各テストの開始時にデフォルトで InputMode.Touch が適用され、決定論が確保され、テスト間で入力モードの状態が漏洩するのを防ぎます。

ComposeUiTestConfig は Compose テスト v2 API の一部であり、デフォルトで StandardTestDispatcher を使用します。テストで v1 API を使用している場合は、ComposeUiTestConfig を導入する前に v2 テスト API に移行するをご覧ください。

ComposeUiTestConfig への移行

テストでセットアップ関数を作成するためのオーバーロードのうち、個々の構成パラメータ(effectContext、runTestContext、testTimeout など)を受け入れるいくつかのオーバーロードは非推奨になりました。次の例に示すように、代わりに ComposeUiTestConfig を使用するようにテストを更新します。

デフォルトの入力モード

テストの開始前にインストルメンテーション API を介して構成された非タップ入力モードに依存している場合、移行中にテストが失敗することがあります。テストのセットアップ関数内では、決定性を高め、状態の漏洩を防ぐため、各テストの開始時にデフォルトで InputMode.Touch が適用されます。これにより、周囲のデバイスの状態とテスト前のセットアップがオーバーライドされます。

この問題を解決するには、ComposeUiTestConfig で必要な入力モードを指定します。

class FocusTest {
    @get:Rule
    val rule = createComposeRule(
        config = ComposeUiTestConfig(inputMode = InputMode.Keyboard)
    )

    @Test
    fun testFocus() {}
}

テストクラス全体ではなく、個々のテストケースの入力モードを構成するには、ComposeUiTestConfig を runComposeUiTest に渡します。

class FocusTest {
    @Test
    fun testTouchMode() = runComposeUiTest {
        // Runs with the default InputMode.Touch
    }

    @Test
    fun testKeyboardMode() = runComposeUiTest(
        ComposeUiTestConfig(inputMode = InputMode.Keyboard)
    ) {
        // Runs with InputMode.Keyboard
    }
}

その他の移行に関する問題と解決策については、一般的なエラーとその修正方法をご覧ください。

v2 テスト API に移行する

v2 API にアップグレードする場合は、通常、検索と置換を使用してパッケージのインポートを更新し、新しいディスパッチャの変更を適用できます。

または、次のプロンプトを使用して、Compose テスト API の v2 への移行を Gemini に相談します。

v1 テスト API から v2 テスト API に移行する

このプロンプトは、このガイドを使用して v2 テスト API に移行します。

Migrate to Compose testing v2 APIs using the official
migration guide.

AI プロンプトの使用

AI プロンプトは、Gemini in Android Studio 内で使用することを目的としています。

Gemini in Android Studio の詳細については、https://developer.android.com/studio/gemini/overview をご覧ください。

次の表を使用して、非推奨の v1 API を v2 の代替 API にマッピングします。

非推奨(v1)

交換(v2)

androidx.compose.ui.test.junit4.createComposeRule

androidx.compose.ui.test.junit4.v2.createComposeRule

androidx.compose.ui.test.junit4.createAndroidComposeRule

androidx.compose.ui.test.junit4.v2.createAndroidComposeRule

androidx.compose.ui.test.junit4.createEmptyComposeRule

androidx.compose.ui.test.junit4.v2.createEmptyComposeRule

androidx.compose.ui.test.junit4.AndroidComposeTestRule

androidx.compose.ui.test.junit4.v2.AndroidComposeTestRule

androidx.compose.ui.test.runComposeUiTest

androidx.compose.ui.test.v2.runComposeUiTest

androidx.compose.ui.test.runAndroidComposeUiTest

androidx.compose.ui.test.v2.runAndroidComposeUiTest

androidx.compose.ui.test.runEmptyComposeUiTest

androidx.compose.ui.test.v2.runEmptyComposeUiTest

androidx.compose.ui.test.AndroidComposeUiTestEnvironment

androidx.compose.ui.test.v2.AndroidComposeUiTestEnvironment

下位互換性と例外

既存の v1 API は非推奨になりましたが、既存の動作を維持し、互換性を破る変更を防ぐために、引き続き UnconfinedTestDispatcher を使用してください。

デフォルトの動作が変更された唯一の例外は次のとおりです。

AndroidComposeUiTestEnvironment クラスでコンポジションを実行するために使用されるデフォルトのテスト ディスパッチャが、UnconfinedTestDispatcher から StandardTestDispatcher に切り替わりました。これは、コンストラクタまたはサブクラス AndroidComposeUiTestEnvironment を使用してインスタンスを作成し、そのコンストラクタを呼び出すケースに影響します。

主な変更点: コルーチンの実行への影響

API の v1 と v2 の主な違いは、コルーチンのディスパッチ方法です。

  • v1 API(UnconfinedTestDispatcher): コルーチンが起動されると、現在のスレッドですぐに実行され、多くの場合、次のテストコード行が実行される前に終了していました。本番環境の動作とは異なり、この即時実行では、実際のアプリケーションで発生する実際のタイミングの問題や競合状態が意図せずマスクされる可能性があります。
  • v2 API(StandardTestDispatcher): コルーチンが起動されると、キューに入れられ、テストが仮想クロックを明示的に進めるまで実行されません。標準の Compose テスト API(waitForIdle() など)はすでにこの同期を処理しているため、これらの標準 API を利用するほとんどのテストは、変更なしで引き続き動作します。

一般的なエラーとその修正方法

v2 へのアップグレード後にテストが失敗する場合は、次のパターンに該当する可能性があります。

  • 失敗: タスクを起動する(たとえば、ViewModel がデータを読み込む)が、データがまだ「読み込み中」状態であるため、アサーションがすぐに失敗する。
  • 原因: v2 API では、コルーチンはすぐに実行されるのではなく、キューに入れられます。タスクはキューに追加されましたが、結果が確認される前に実際には実行されませんでした。
  • 修正: 時間を明示的に進めます。v2 ディスパッチャに、作業を実行するタイミングを明示的に指示する必要があります。

従来のアプローチ

v1 では、タスクが起動してすぐに終了しました。v2 では、loadData() がまだ実際に実行されていないため、次のコードは失敗します。

// In v1, this launched and finished immediately.
viewModel.loadData()

// In v2, this fails because loadData() hasn't actually run yet!
assertEquals(Success, viewModel.state.value)

アサートする前に、waitForIdle または runOnIdle を使用してキューに格納されたタスクを実行します。

オプション 1: waitForIdle を使用すると、UI がアイドル状態になるまでクロックが進み、コルーチンが実行されたことが確認されます。

viewModel.loadData()

// Explicitly run all queued tasks
composeTestRule.waitForIdle()

assertEquals(Success, viewModel.state.value)

オプション 2: runOnIdle を使用すると、UI がアイドル状態になった後に UI スレッドでコードブロックが実行されます。

viewModel.loadData()

// Run the assertion after the UI is idle
composeTestRule.runOnIdle {
    assertEquals(Success, viewModel.state.value)
}

手動同期

自動進行が無効になっている場合など、手動同期を伴うシナリオでは、テストクロックが一時停止されるため、コルーチンを起動してもすぐに実行されることはありません。仮想クロックを進めずにキュー内のコルーチンを実行するには、runCurrent() API を使用します。これにより、現在の仮想時間にスケジュールされたタスクが実行されます。

composeTestRule.mainClock.scheduler.runCurrent()

UI が安定するまでテストクロックを進める waitForIdle() とは対照的に、runCurrent() は現在の仮想時間を維持しながら保留中のタスクを実行します。この動作により、クロックがアイドル状態に進んだ場合にスキップされる中間状態の検証が可能になります。

テスト環境で使用される基盤となるテスト スケジューラが公開されます。このスケジューラは、Kotlin runTest API と組み合わせて使用して、テストクロックを同期できます。

runComposeUiTest への移行

Kotlin runTest API とともに Compose テスト API を使用している場合は、runComposeUiTest に切り替えることを強くおすすめします。

従来のアプローチ

createComposeRule を runTest と組み合わせて使用すると、Compose 用とテスト コルーチン スコープ用の 2 つの別々のクロックが作成されます。この構成では、テスト スケジューラを手動で同期する必要があります。

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun testWithCoroutines() {
    composeTestRule.setContent {
        var status by remember { mutableStateOf("Loading...") }
        LaunchedEffect(Unit) {
            delay(1000)
            status = "Done!"
        }
        Text(text = status)
    }

    // NOT RECOMMENDED
    // Fails: runTest creates a new, separate scheduler.
    // Advancing time here does NOT advance the compose clock.
    // To fix this without migrating, you would need to share the scheduler
    // by passing 'composeTestRule.mainClock.scheduler' to runTest.
    runTest {
        composeTestRule.onNodeWithText("Loading...").assertIsDisplayed()
        advanceTimeBy(1000)
        composeTestRule.onNodeWithText("Done!").assertIsDisplayed()
    }
}

runComposeUiTest API は、独自の runTest スコープ内でテストブロックを自動的に実行します。テスト クロックは Compose 環境と同期されるため、スケジューラを手動で管理する必要がなくなりました。

    @Test
    fun testWithCoroutines() = runComposeUiTest {
        setContent {
            var status by remember { mutableStateOf("Loading...") }
            LaunchedEffect(Unit) {
                delay(1000)
                status = "Done!"
            }
            Text(text = status)
        }

        onNodeWithText("Loading...").assertIsDisplayed()
        mainClock.advanceTimeBy(1000 + 16 /* Frame buffer */)
        onNodeWithText("Done!").assertIsDisplayed()
    }
}