Compose 測試 API 的 v2 版本 (createComposeRule、createAndroidComposeRule、runComposeUiTest、runAndroidComposeUiTest 等) 現已推出,可進一步控管協同程式執行。這項更新不會複製整個 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 整合至單一物件。
設定模型也會管理 inputMode。Compose 測試 v2 API 預設會在每次測試開始時強制執行 InputMode.Touch,確保結果確定,並防止輸入模式狀態在測試之間外洩。
ComposeUiTestConfig 是 Compose 測試 v2 API 的一部分,預設使用 StandardTestDispatcher。如果測試使用第 1 版 API,請先參閱「遷移至第 2 版測試 API」,再採用 ComposeUiTestConfig。
遷移至 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 } }
如需其他遷移問題和解決方法,請參閱「常見失敗原因和修正方式」。
遷移至第 2 版測試 API
升級至 v2 API 時,您通常可以使用「Find + Replace」更新套件匯入作業,並採用新的調度工具變更。
或者,您也可以使用下列提示,要求 Gemini 遷移至 Compose 測試 API 第 2 版:
AI 提示
從第 1 版測試 API 遷移至第 2 版測試 API
本提示將使用這份指南,遷移至第 2 版測試 API。
Migrate to Compose testing v2 APIs using the official
migration guide.請使用下表,將已淘汰的 v1 API 對應至 v2 替代項目:
已淘汰 (v1) |
取代選項 (v2) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
回溯相容性和例外狀況
現有的第 1 版 API 已淘汰,但請繼續使用 UnconfinedTestDispatcher,以維持現有行為並避免破壞性變更。
只有下列情況會變更預設行為:
在 AndroidComposeUiTestEnvironment 類別中,用於執行組合的預設測試調度器已從 UnconfinedTestDispatcher 切換為 StandardTestDispatcher。這會影響您使用建構函式或子類別 AndroidComposeUiTestEnvironment 建立例項,並呼叫該建構函式的情況。
重要異動:對協同程式執行的影響
API 第 1 版和第 2 版的主要差異在於協同程式的調度方式:
- v1 API (
UnconfinedTestDispatcher):啟動協同程式時,該程式會立即在目前的執行緒上執行,通常會在下一行測試程式碼執行前完成。與實際工作環境行為不同,這種立即執行方式可能會不小心掩蓋實際應用程式中發生的實際時間問題或競爭狀況。 - v2 API (
StandardTestDispatcher):協同程式啟動後會排入佇列,直到測試明確推進虛擬時鐘才會執行。標準 Compose 測試 API (例如waitForIdle()) 已處理這項同步處理作業,因此大多數依賴這些標準 API 的測試應可繼續運作,不需進行任何變更。
未通過審查的常見原因和解決方法
如果升級至第 2 版後測試失敗,可能符合下列模式:
- 失敗:您啟動一項工作 (例如 ViewModel 載入資料),但斷言立即失敗,因為資料仍處於「載入中」狀態。
- 原因:使用第 2 版 API 時,協同程式會排入佇列,而非立即執行。工作已排入佇列,但在檢查結果前從未實際執行。
- 修正:明確推進時間。您必須明確告知第 2 版調度工具何時執行工作。
先前做法
在第 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() } }