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 保持一致,並明確控管協同程式執行順序。
API 對應
升級至 v2 API 時,您通常可以使用「尋找及取代」更新套件匯入項目,並採用新的調度器變更。
或者,你也可以使用下列提示詞,要求 Gemini 將 Compose 測試 API 遷移至第 2 版:
AI 提示詞
從 v1 測試 API 遷移至 v2 測試 API
系統會使用這項提示,引導您遷移至 v2 測試 API。
Migrate to Compose testing v2 APIs using the official
migration guide.請參閱下表,瞭解已淘汰的 v1 API 與 v2 替代 API 的對應關係:
已淘汰 (第 1 版) |
取代 (v2) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
回溯相容性和例外狀況
現有的第 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)
建議做法
在進行判斷前,請使用 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()
waitForIdle() 會推進測試時鐘,直到 UI 穩定為止,而 runCurrent() 則會執行待處理的工作,同時維持目前的虛擬時間。如果時鐘前進至閒置狀態,系統會略過中間狀態的驗證,但這項行為可啟用驗證。
測試環境中使用的基礎測試排程器會公開。這個排程器可搭配 Kotlin runTest API 使用,同步處理測試時鐘。
遷移至 runComposeUiTest
如果您同時使用 Compose 測試 API 和 Kotlin runTest API,強烈建議改用 runComposeUiTest。
先前做法
搭配使用 createComposeRule 和 runTest 會建立兩個不同的時鐘:一個用於 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() } }