遷移至 v2 測試 API

Compose 測試 API 的 v2 版本 (createComposeRulecreateAndroidComposeRulerunComposeUiTestrunAndroidComposeUiTest 等) 現已推出,可提升對協同程式執行的控制。這項更新不會複製整個 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 保持一致,並明確控管協同程式執行順序。

API 對應

升級至 v2 API 時,您通常可以使用「尋找及取代」更新套件匯入項目,並採用新的調度器變更。

或者,你也可以使用下列提示詞,要求 Gemini 將 Compose 測試 API 遷移至第 2 版:

從 v1 測試 API 遷移至 v2 測試 API

系統會使用這項提示,引導您遷移至 v2 測試 API。

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

使用 AI 提示

AI 提示詞僅供在 Gemini 版 Android Studio 使用。

如要進一步瞭解 Studio 中的 Gemini,請前往:https://developer.android.com/studio/gemini/overview

請參閱下表,瞭解已淘汰的 v1 API 與 v2 替代 API 的對應關係:

已淘汰 (第 1 版)

取代 (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

回溯相容性和例外狀況

現有的第 1 版 API 已淘汰,但請繼續使用 UnconfinedTestDispatcher,以維持現有行為並避免重大變更。

預設行為變更的唯一例外狀況如下:

用於在 AndroidComposeUiTestEnvironment 類別中執行組合的預設測試調度器,已從 UnconfinedTestDispatcher 切換為 StandardTestDispatcher。如果您使用建構函式建立例項,或是將 AndroidComposeUiTestEnvironment 子類別化並呼叫該建構函式,就會受到影響。

主要異動:對協同程式執行的影響

第 1 版和第 2 版 API 的主要差異在於協同程式的調度方式:

  • v1 API (UnconfinedTestDispatcher):啟動協同程式後,系統會立即在目前執行緒上執行,通常會在下一行測試程式碼執行前完成。與實際工作環境中的行為不同,這種立即執行可能會無意間遮蓋實際時間問題或競爭條件,而這些問題或條件會發生在實際運作的應用程式中。
  • v2 API (StandardTestDispatcher):啟動協同程式時,系統會將其排入佇列,並在測試明確推進虛擬時鐘前不會執行。標準 Compose 測試 API (例如 waitForIdle()) 已處理這項同步作業,因此大部分依賴這些標準 API 的測試應可繼續運作,不需進行任何變更。

未通過審查的常見原因和解決方法

升級至 v2 後,如果測試失敗,可能會有下列模式:

  • 失敗:您啟動工作 (例如 ViewModel 載入資料),但由於資料仍處於「載入中」狀態,因此斷言會立即失敗。
  • 原因:使用第 2 版 API 時,系統會將協同程式排入佇列,而非立即執行。工作已加入佇列,但在檢查結果前從未實際執行。
  • 修正:明確推進時間。您必須明確告知 v2 派送器何時執行工作。

先前做法

在第 1 版中,工作會立即啟動並完成。在第 2 版中,下列程式碼會失敗,因為 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)

在進行判斷前,請使用 waitForIdlerunOnIdle 執行佇列工作。

選項 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()

waitForIdle() 會推進測試時鐘,直到 UI 穩定為止,而 runCurrent() 則會執行待處理的工作,同時維持目前的虛擬時間。如果時鐘前進至閒置狀態,系統會略過中間狀態的驗證,但這項行為可啟用驗證。

測試環境中使用的基礎測試排程器會公開。這個排程器可搭配 Kotlin runTest API 使用,同步處理測試時鐘。

遷移至 runComposeUiTest

如果您同時使用 Compose 測試 API 和 Kotlin runTest API,強烈建議改用 runComposeUiTest

先前做法

搭配使用 createComposeRulerunTest 會建立兩個不同的時鐘:一個用於 Compose,另一個用於測試協同程式範圍。這項設定可能會強制您手動同步處理測試排程器。

@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()
    }
}