Đồng bộ hoá chương trình kiểm thử

Các quy trình kiểm thử Compose được đồng bộ hoá theo mặc định với giao diện người dùng. Khi bạn gọi một câu nhận định hoặc một hành động bằng ComposeTestRule, quy trình kiểm thử sẽ được đồng bộ hoá trước đó, chờ cho đến khi cây giao diện người dùng không hoạt động.

Thông thường, bạn không phải thực hiện bất kỳ hành động nào. Tuy nhiên, có một số trường hợp phức tạp (edge case) mà bạn nên biết.

Khi một quy trình kiểm thử được đồng bộ hoá, ứng dụng Compose của bạn được đẩy nhanh thời gian bằng cách sử dụng đồng hồ ảo. Điều này có nghĩa là các quy trình kiểm thử Compose không chạy trong thời gian thực, cho nên các quy trình kiểm thử này có thể được thực hiện nhanh nhất trong khả năng.

Tuy nhiên, nếu bạn không sử dụng các phương thức đồng bộ hoá quy trình kiểm thử thì sẽ không xảy ra quá trình kết hợp lại và giao diện người dùng sẽ tạm dừng.

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

Xin lưu ý rằng yêu cầu này chỉ áp dụng cho hệ phân cấp Compose và không áp dụng cho phần còn lại của ứng dụng.

Tắt tính năng tự động đồng bộ hoá

Khi bạn gọi một câu nhận định (assertion) hoặc hành động thông qua ComposeTestRule chẳng hạn như assertExists(), quy trình kiểm thử của bạn sẽ được đồng bộ hoá với giao diện người dùng Compose. Trong một số trường hợp, bạn có thể dừng quá trình đồng bộ hoá này và tự điều khiển đồng hồ. Ví dụ: bạn có thể kiểm soát thời gian để chụp ảnh màn hình chính xác của một ảnh động tại thời điểm giao diện người dùng vẫn bận. Để tắt tính năng đồng bộ hoá tự động, hãy đặt thuộc tính autoAdvance trong mainClock thành false:

composeTestRule.mainClock.autoAdvance = false

Thông thường, sau đó bạn sẽ tự chuyển đến thời gian của mình. Bạn có thể chuyển đến chính xác một khung bằng advanceTimeByFrame() hoặc chuyển đến một khoảng thời gian cụ thể bằng advanceTimeBy():

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

Tài nguyên không tải

Compose có thể đồng bộ hoá các quy trình kiểm thử và giao diện người dùng để mọi hành động và câu nhận định (assertion) được thực hiện ở trạng thái rảnh, chờ hoặc đẩy nhanh đồng hồ khi cần. Tuy nhiên, một số tác vụ không đồng bộ có kết quả ảnh hưởng đến trạng thái giao diện người dùng có thể chạy ở chế độ nền và quy trình kiểm thử không nhận biết được điều này.

Tạo và đăng ký các tài nguyên không đồng bộ này trong quy trình kiểm thử để các tài nguyên này có thể được xem xét khi quyết định xem ứng dụng đang kiểm thử đang bận hay ở trạng thái rảnh. Bạn không phải làm gì, trừ trường hợp cần đăng ký tài nguyên không đồng bộ bổ sung, chẳng hạn như khi chạy công việc trong nền không được đồng bộ hoá với Espresso hoặc Compose.

API này rất giống với tài nguyên không đồng bộ của Espresso để cho biết rằng đối tượng đang kiểm thử có đang rảnh hay bận. Sử dụng quy tắc kiểm thử Compose để đăng ký phương thức triển khai IdlingResource.

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

Đồng bộ hoá thủ công

Trong một số trường hợp, bạn phải đồng bộ hoá giao diện người dùng Compose với các phần khác trong quy trình kiểm thử hoặc ứng dụng đang kiểm thử.

Hàm waitForIdle() chờ Compose ở trạng thái rảnh, nhưng hàm này còn tuỳ thuộc vào thuộc tính 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.

Xin lưu ý rằng trong cả hai trường hợp, waitForIdle() cũng sẽ đợi các lượt vẽ và bố cục đang chờ xử lý.

Ngoài ra, bạn có thể chuyển đồng hồ cho đến khi đáp ứng một điều kiện nhất định bằng advanceTimeUntil().

composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }

Lưu ý rằng điều kiện trên phải được kiểm tra trạng thái có thể bị ảnh hưởng bởi đồng hồ này (chỉ hoạt động với trạng thái Compose).

Tối ưu hoá thử nghiệm ảnh động

Khi kiểm thử ảnh động có độ chân thực cao, bạn thường cần tắt tính năng tự động chuyển tiếp và thực hiện từng bước theo cách thủ công để xác nhận trạng thái giao diện người dùng trung gian. Đối với các vòng lặp từng khung hình cụ thể này, hãy sử dụng phương thức runWithoutImplicitWait để thực thi các câu nhận định. Các truy vấn nút tiêu chuẩn (như onNodeWithTag hoặc fetchSemanticsNode) kích hoạt các hoạt động đồng bộ hoá ngầm định dư thừa khi bạn đang điều khiển đồng hồ theo cách thủ công, vì vậy, việc bỏ qua các truy vấn này sẽ giúp tăng tốc đáng kể thời gian chạy kiểm thử.

Nguyên tắc sử dụng

  • Quản lý đồng hồ theo cách thủ công: Sử dụng API này khi mainClock.autoAdvance được đặt thành false và giao diện người dùng ở trạng thái ổn định, đã biết cho khung hiện tại.
  • Thực thi luồng giao diện người dùng: Để đảm bảo độ ổn định của cây giao diện người dùng, hãy gọi runWithoutImplicitWait trên luồng giao diện người dùng, chẳng hạn như bằng runOnUiThread. Việc chạy mã này ngoài luồng giao diện người dùng sẽ khiến kiểm thử của bạn gặp phải tình huống tương tranh và đọc trạng thái cũ.
  • Xác nhận chỉ đọc: Khối này chỉ được chứa các câu nhận định chỉ đọc. Mọi hành động làm thay đổi trạng thái phải được thực hiện bên ngoài khối này.

Ví dụ

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

Đồng bộ hoá luồng chính

Tính năng kiểm thử Compose hiện hỗ trợ đồng bộ hoá luồng chính, cho phép bạn gọi waitForIdle một cách an toàn, đồng thời mở rộng các hành động và câu nhận định về giao diện người dùng Compose ngay từ luồng chính.

Trước đây, quy trình kiểm thử Compose thực thi nghiêm ngặt mô hình hai luồng: phiên chạy thử nghiệm diễn ra trên một luồng kiểm thử trong nền, trong khi các bản cập nhật giao diện người dùng diễn ra trên luồng chính. Việc gọi các phương thức đồng bộ hoá như waitForIdle hoặc runOnIdle từ luồng chính (ví dụ: bên trong khối runOnUiThread) sẽ gửi một IllegalStateException vì khung này thực thi các quy trình kiểm tra luồng nghiêm ngặt để ngăn chặn việc đồng bộ hoá luồng chính.

Khi bật tính năng đồng bộ hoá luồng chính, khung kiểm thử Compose hiện có thể tăng tốc đồng hồ và xử lý công việc đang chờ xử lý ngay cả khi các lệnh gọi chặn được thực hiện trên luồng chính.

Trường hợp nên sử dụng tính năng đồng bộ hoá luồng chính

Mặc dù việc duy trì kiểm thử trên luồng nền vẫn là tiêu chuẩn cho các kiểm thử Compose thuần tuý, nhưng việc đồng bộ hoá luồng chính lại rất có lợi trong một số trường hợp cụ thể:

  • Khả năng tương tác phức tạp với Khung hiển thị: Khi kiểm thử giao diện người dùng kết hợp chứa cả Compose và Khung hiển thị Android cũ, việc thao tác với Khung hiển thị thường yêu cầu chạy trên luồng chính. Giờ đây, bạn có thể tương tác với Khung hiển thị và xác nhận trên các nút Compose theo tuần tự mà không cần liên tục chuyển đổi ngữ cảnh luồng.
  • Thay đổi trạng thái đồng bộ: Nếu cấu trúc của bạn dựa vào các phần tử giữ trạng thái liên kết chặt chẽ với luồng chính, thì giờ đây, bạn có thể thay đổi trạng thái và ngay lập tức chờ giao diện người dùng Compose ổn định mà không cần rời khỏi luồng chính.
  • Trình chạy kiểm thử tuỳ chỉnh: Nếu bạn đang xây dựng cơ sở hạ tầng kiểm thử tuỳ chỉnh hoặc sử dụng môi trường mà trình chạy kiểm thử vốn thực thi trên luồng chính, thì các kiểm thử Compose hiện sẽ thực thi một cách rõ ràng mà không cần uỷ quyền luồng nền.

Ví dụ

Trước đây, do việc đồng bộ hoá bị nghiêm cấm trên luồng chính, nên nhà phát triển phải chuyển đổi qua lại giữa luồng trình chạy kiểm thử trong nền và luồng giao diện người dùng, dẫn đến các kiểm thử rời rạc:

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

Khi bật tính năng đồng bộ hoá luồng chính, các câu nhận định cho hệ phân cấp Compose và Khung hiển thị có thể được thực thi trong cùng một khối:

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

Chờ điều kiện

Mọi điều kiện phụ thuộc vào công việc bên ngoài, chẳng hạn như tải dữ liệu hoặc đo lường hay vẽ của Android (nghĩa là đo lường hoặc vẽ bên ngoài Compose), phải sử dụng khái niệm chung hơn, chẳng hạn như waitUntil():

composeTestRule.waitUntil(timeoutMs) { condition }

Bạn cũng có thể sử dụng bất kỳ trình trợ giúp waitUntil nào:

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

Tài nguyên khác

  • Kiểm thử ứng dụng trên Android: Trang đích chính về kiểm thử trên Android cung cấp thông tin tổng quan về các nguyên tắc cơ bản và kỹ thuật kiểm thử.
  • Nguyên tắc cơ bản về kiểm thử: Tìm hiểu thêm về các khái niệm cốt lõi đằng sau việc kiểm thử một ứng dụng Android.
  • Kiểm thử cục bộ: Bạn có thể chạy một số bài kiểm thử cục bộ trên máy trạm của riêng mình.
  • Kiểm thử đo lường: Bạn cũng nên chạy kiểm thử đo lường. Tức là các kiểm thử chạy trực tiếp trên thiết bị.
  • Tích hợp liên tục: Tính năng tích hợp liên tục cho phép bạn tích hợp các thử nghiệm vào quy trình triển khai.
  • Kiểm thử nhiều kích thước màn hình: Vì người dùng có thể sử dụng nhiều thiết bị, nên bạn cần kiểm thử đối với nhiều kích thước màn hình.
  • Espresso: Mặc dù được thiết kế cho giao diện người dùng dựa trên Khung hiển thị, nhưng kiến thức về Espresso vẫn có thể hữu ích cho một số khía cạnh của hoạt động kiểm thử trong Compose.