同步處理測試

根據預設,系統會與您的使用者介面同步處理撰寫測試。透過 ComposeTestRule 呼叫宣告或動作時,系統會預先同步處理測試,直到 UI 樹狀結構閒置為止。

一般來說,您不必採取任何行動。但仍有一些邊緣案例需要知道。

測試進行同步處理後,Compose 應用程式就會使用虛擬時鐘適時完成進階。這表示 Compose 測試不會即時執行,因此可以盡可能快速通過。

不過,如果您沒有使用同步處理測試的方法,則不會發生重覆現象,且 UI 會暫停。

@Test
fun counterTest() {
    val myCounter = mutableStateOf(0) // State that can cause recompositions.
    var lastSeenValue = 0 // Used to track recompositions.
    composeTestRule.setContent {
        Text(myCounter.value.toString())
        lastSeenValue = myCounter.value
    }
    myCounter.value = 1 // The state changes, but there is no recomposition.

    // Fails because nothing triggered a recomposition.
    assertTrue(lastSeenValue == 1)

    // Passes because the assertion triggers recomposition.
    composeTestRule.onNodeWithText("1").assertExists()
}

請注意,這項規定僅適用於 Compose 階層,應用程式的其他部分並不適用。

停用自動同步功能

透過 ComposeTestRule (例如 assertExists()) 呼叫宣告或動作時,您的測試會與 Compose UI 同步處理。在某些情況下,您可能會想要停止同步處理並自行控管時鐘。舉例來說,您可以控制在 UI 忙碌期間,為動畫擷取準確螢幕截圖的時間。如要停用自動同步功能,請將 mainClock 中的 autoAdvance 屬性設為 false

composeTestRule.mainClock.autoAdvance = false

一般而言,您需要自行安排時間。您可以透過 advanceTimeByFrame() 將特定影格前進,也可以使用 advanceTimeBy() 按照特定時間長度快一點:

composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)

閒置資源

Compose 可以同步處理測試和 UI,讓所有動作和宣告都以閒置狀態完成,然後視需要等待或往前調整時鐘。但是,某些影響 UI 狀態的非同步作業可能會在背景執行,但測試無法得知這些結果。

您可以在測試中建立及註冊這些閒置資源,以便判斷要測試的應用程式是否忙碌中或閒置。除非您需要註冊額外的閒置資源,否則不需要採取行動。舉例來說,如果您執行的背景工作並未與 Espresso 或 Compose 同步處理。

這個 API 與 Espresso 的 Idling Resources 非常類似,可讓您表示接受測試的實體是閒置或忙碌。您可以使用「撰寫」測試規則來註冊 IdlingResource 的實作。

composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)

手動同步處理

在某些情況下,您必須將 Compose UI 與其他測試或您要測試的應用程式同步處理。

waitForIdle() 函式會等待 Compose 閒置,但函式取決於 autoAdvance 屬性:

composeTestRule.mainClock.autoAdvance = true // Default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle.

composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for idling resources to become idle.

請注意,在這兩種情況下,waitForIdle() 也會等候待處理的繪製和版面配置傳遞

此外,您也可以將時鐘加至 advanceTimeUntil() 符合特定條件為止。

composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }

請注意,指定的條件只能查看受到這個時鐘影響的狀態 (這個狀態僅適用於「撰寫」狀態)。

最佳化動畫測試

測試高保真動畫時,您通常需要停用自動前進功能,並手動逐步檢查影格,以判斷中繼 UI 狀態。如要針對這些特定逐格迴圈執行斷言,請使用 runWithoutImplicitWait 方法。標準節點查詢 (例如 onNodeWithTagfetchSemanticsNode) 會觸發隱含同步,但手動控制時鐘時,這些同步作業會多餘,因此略過這些作業可大幅加快測試執行時間。

使用指南

  • 手動時鐘管理:如果 mainClock.autoAdvance 設為 false,且 UI 處於目前影格的已知穩定狀態,請使用這個 API。
  • UI 執行緒執行:為確保 UI 樹狀結構穩定,請在 UI 執行緒上呼叫 runWithoutImplicitWait,例如使用 runOnUiThread。如果從 UI 執行緒執行,測試就會暴露於競爭條件和過時的狀態讀取作業。
  • 唯讀斷言:區塊應嚴格包含唯讀斷言。任何會改變狀態的動作都應在這個區塊外執行。

範例

@Test
fun runWithoutImplicitWaitSample() = runComposeUiTest {
    setContent { MainScreen() }
    mainClock.autoAdvance = false

    // Trigger an animation
    onNodeWithText("Start Animation").performClick()

    // Step through the animation frame-by-frame
    while (hasPendingWork()) {
        mainClock.advanceTimeByFrame()
        waitForIdle()
        runOnUiThread {
            // Suppress implicit synchronization inside this block to avoid redundant
            // waits on each node query, making the frame assertions execute much faster.
            runWithoutImplicitWait {
                val box1 = onNodeWithTag("Box1").fetchSemanticsNode()
                val box2 = onNodeWithTag("Box2").fetchSemanticsNode()
                val box3 = onNodeWithTag("Box3").fetchSemanticsNode()

                // Assert the exact intermediate state of all three properties for this frame
                assert(box1.boundsInRoot.right <= box2.boundsInRoot.left)
                assert(box2.boundsInRoot.right <= box3.boundsInRoot.left)
            }
        }
    }
}

主執行緒同步

Compose 測試現在支援主執行緒同步處理,可讓您直接從主執行緒安全地呼叫 waitForIdle,並擴充 Compose UI 動作和斷言。

先前,Compose 測試嚴格採用雙執行緒模型:測試執行作業會在背景測試執行緒上進行,而 UI 更新作業則會在主執行緒上進行。從主執行緒呼叫 waitForIdlerunOnIdle 等同步處理方法 (例如在 runOnUiThread 區塊內),會擲回 IllegalStateException,因為架構會強制執行嚴格的執行緒檢查,防止主執行緒同步處理。

啟用主執行緒同步後,即使在主執行緒上發出封鎖呼叫,Compose 測試架構現在也能推進時鐘並處理待處理的工作。

使用主執行緒同步處理的時機

雖然將測試保留在背景執行緒中,仍是純 Compose 測試的標準做法,但在某些特定情境中,主要執行緒同步處理具有高度優勢:

  • 複雜的 View 互通性:測試同時包含 Compose 和舊版 Android View 的混合式 UI 時,如要操控 View,通常需要在主執行緒上執行。您現在可以與 Views 互動,並依序對 Compose 節點進行斷言,不必不斷切換執行緒環境。
  • 同步狀態突變:如果您的架構嚴格依附於主執行緒繫結的狀態持有者,現在可以突變狀態,並立即等待 Compose UI 結算,無須離開主執行緒。
  • 自訂測試執行器:如果您要建構自訂測試基礎架構,或使用測試執行器本質上會在主要執行緒執行的環境,Compose 測試現在可乾淨地執行,不必委派背景執行緒。

範例

過去,由於主執行緒嚴格禁止同步處理,開發人員必須在背景測試執行緒和 UI 執行緒之間來回跳動,導致測試不連貫:

@Test
fun testBidirectionalInteropUIUpdates_old() {
    val scenario = launchFragmentInContainer<InteropFragment>()
    composeTestRule.waitForIdle()
    scenario.onFragment { fragment ->
        fragment.legacyButton.performClick()
    }
    // Jump to Test Thread to verify state settles inside compose
    composeTestRule.waitForIdle()
    composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed()
    composeTestRule.onNodeWithText("Increment Legacy TextView").performClick()
    composeTestRule.waitForIdle()
    // Jump back to Main Thread to verify target view state settles
    scenario.onFragment { fragment ->
        assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1")
    }
}

啟用主要執行緒同步後,即可在同一個區塊中執行 Compose 和 View 層級的斷言:

@Test
fun testBidirectionalInteropUIUpdates_new() {
    val scenario = launchFragmentInContainer<InteropFragment>()
    composeTestRule.waitForIdle()
    scenario.onFragment { fragment ->
        fragment.legacyButton.performClick()
        composeTestRule.waitForIdle()
        composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed()
        composeTestRule.onNodeWithText("Increment Legacy TextView").performClick()
        composeTestRule.waitForIdle()
        assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1")
    }
}

等待條件

任何取決於外部作業的條件,例如資料載入或 Android 的測量或繪圖 (也就是在 Compose 外部測量或繪製),都應使用較一般的概念,例如 waitUntil()

composeTestRule.waitUntil(timeoutMs) { condition }

您也可以使用任何 waitUntil 輔助程式

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

其他資源

  • 在 Android 上測試應用程式:這個主要的 Android 測試到達網頁,提供更廣泛的測試基礎知識和技術。
  • 測試基礎知識進一步瞭解測試 Android 應用程式背後的概念。
  • 本機測試您可以在自己的工作站上,在本機執行部分測試。
  • 檢測設備測試建議您也執行檢測設備測試。也就是直接在裝置端執行的測試。
  • 持續整合 持續整合可讓您將測試整合至部署管道。
  • 測試不同螢幕大小使用者可選擇的裝置種類繁多,因此您應測試不同螢幕大小。
  • Espresso:雖然 Espresso 是專為以 View 為基礎的 UI 設計,但您仍可運用相關知識,測試 Compose 的某些層面。