Compose テスト API の v2 バージョン(createComposeRule、createAndroidComposeRule、runComposeUiTest、runAndroidComposeUiTest など)が利用可能になり、コルーチンの実行をより細かく制御できるようになりました。この更新では、API サーフェス全体が複製されるわけではなく、テスト環境を確立する API のみが更新されます。
v1 API は非推奨であり、新しい API への移行を強くおすすめします。移行すると、テストが標準のコルーチン動作に沿っていることが検証され、将来の互換性の問題が回避されます。非推奨の v1 API のリストについては、API マッピングをご覧ください。
これらの変更は androidx.compose.ui:ui-test-junit4:1.11.0-alpha03+ と androidx.compose.ui:ui-test:1.11.0-alpha03+ に含まれています。
v1 API は UnconfinedTestDispatcher に依存していましたが、v2 API は実行中のコンポジションにデフォルトで StandardTestDispatcher を使用します。この変更により、Compose テストの動作が標準の runTest API と一致し、コルーチンの実行順序を明示的に制御できるようになります。
テスト環境を構成する
Compose テスト v2 API は、ComposeUiTestConfig を使用してテスト環境をカスタマイズします。createComposeRule、runComposeUiTest、その他の関連 API などのテストのセットアップ関数を作成する API は、ComposeUiTestConfig を受け入れます。この構成オブジェクトは、effectContext、runTestContext、testTimeout などの環境関連の API を単一のオブジェクトに統合します。
構成モデルは inputMode も管理します。Compose テスト v2 API は、各テストの開始時にデフォルトで InputMode.Touch を適用して、決定性を確保し、テスト間で入力モードの状態が漏洩しないようにします。
ComposeUiTestConfig は Compose Test v2 API の一部であり、デフォルトで StandardTestDispatcher を使用します。テストで v1 API を使用している場合は、ComposeUiTestConfig を採用する前に、v2 テスト API への移行をご覧ください。
ComposeUiTestConfig への移行
テストでセットアップ関数を作成するためのオーバーロード内で、effectContext、runTestContext、testTimeout などの個々の構成パラメータを受け入れるいくつかのオーバーロードが非推奨になりました。次の例に示すように、ComposeUiTestConfig を使用するようにテストを更新します。
val testConfig = ComposeUiTestConfig( effectContext = EmptyCoroutineContext, runTestContext = EmptyCoroutineContext, testTimeout = 30.seconds ) @get:Rule val rule = createComposeRule(config = testConfig) // OR runComposeUiTest(config = testConfig) {}
デフォルトの入力モード
テストが開始される前に計測 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 にアップグレードする際は、通常、検索と置換を使用してパッケージのインポートを更新し、新しいディスパッチャーの変更を適用できます。
または、次のプロンプトを使用して、Gemini に Compose テスト API の v2 への移行を依頼します。
AI プロンプト
v1 テスト API から v2 テスト API に移行する
このプロンプトでは、このガイドを使用して v2 テスト API に移行します。
Migrate to Compose testing v2 APIs using the official
migration guide.次の表を使用して、非推奨となった v1 API を v2 の代替 API にマッピングします。
非推奨(v1) |
Replacement(v2) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
下位互換性と例外
既存の 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() } }