Định cấu hình môi trường thử nghiệm bằng API kiểm thử phiên bản 2

Các phiên bản v2 của API kiểm thử Compose (createComposeRule, createAndroidComposeRule, runComposeUiTest, runAndroidComposeUiTest, v.v.) hiện đã có sẵn để cải thiện khả năng kiểm soát đối với việc thực thi coroutine. Bản cập nhật này không sao chép toàn bộ nền tảng API; chỉ những API thiết lập môi trường thử nghiệm mới được cập nhật.

Các API phiên bản 1 không còn được dùng nữa và bạn nên di chuyển sang các API mới. Việc di chuyển giúp xác minh rằng các kiểm thử của bạn phù hợp với hành vi coroutine tiêu chuẩn và tránh các vấn đề về khả năng tương thích trong tương lai. Để xem danh sách các API phiên bản 1 không dùng nữa, hãy xem phần Ánh xạ API.

Mặc dù API phiên bản 1 dựa vào UnconfinedTestDispatcher, nhưng API phiên bản 2 sử dụng StandardTestDispatcher theo mặc định cho thành phần đang chạy. Thay đổi này giúp hành vi kiểm thử của Compose phù hợp với các API runTest tiêu chuẩn và cung cấp quyền kiểm soát rõ ràng đối với thứ tự thực thi coroutine.

Định cấu hình môi trường thử nghiệm

API kiểm thử Compose phiên bản 2 sử dụng ComposeUiTestConfig để tuỳ chỉnh môi trường kiểm thử. Các API tạo hàm thiết lập cho kiểm thử, chẳng hạn như createComposeRule, runComposeUiTest và các API liên quan khác, chấp nhận ComposeUiTestConfig. Đối tượng cấu hình này hợp nhất các API liên quan đến môi trường như effectContext, runTestContext và testTimeout thành một đối tượng duy nhất.

Mô hình cấu hình cũng quản lý inputMode. Theo mặc định, các API phiên bản 2 của Compose Test sẽ thực thi InputMode.Touch khi bắt đầu mỗi lần kiểm thử để đảm bảo tính tất định và ngăn trạng thái chế độ nhập rò rỉ giữa các lần kiểm thử.

ComposeUiTestConfig là một phần của API kiểm thử Compose phiên bản 2, sử dụng StandardTestDispatcher theo mặc định. Nếu các chương trình kiểm thử của bạn sử dụng API phiên bản 1, hãy xem phần Di chuyển sang API kiểm thử phiên bản 2 trước khi áp dụng ComposeUiTestConfig.

Di chuyển sang ComposeUiTestConfig

Trong các phương thức nạp chồng để tạo hàm thiết lập trong kiểm thử, một số phương thức nạp chồng chấp nhận các tham số cấu hình riêng lẻ (chẳng hạn như effectContext, runTestContext hoặc testTimeout) không còn được dùng nữa. Hãy cập nhật các bài kiểm thử để sử dụng ComposeUiTestConfig, như trong ví dụ sau:

Chế độ nhập mặc định

Các kiểm thử có thể không thành công trong quá trình di chuyển nếu các kiểm thử đó dựa vào chế độ nhập không chạm được định cấu hình thông qua API đo lường trước khi kiểm thử bắt đầu. Trong các hàm thiết lập cho kiểm thử, hệ thống sẽ thực thi InputMode.Touch theo mặc định khi bắt đầu mỗi kiểm thử để tăng tính tất định và ngăn chặn rò rỉ trạng thái, ghi đè trạng thái thiết bị xung quanh và thiết lập trước kiểm thử.

Để giải quyết vấn đề này, hãy chỉ định chế độ nhập bắt buộc trong ComposeUiTestConfig:

class FocusTest {
    @get:Rule
    val rule = createComposeRule(
        config = ComposeUiTestConfig(inputMode = InputMode.Keyboard)
    )

    @Test
    fun testFocus() {}
}

Để định cấu hình chế độ nhập cho từng trường hợp kiểm thử thay vì toàn bộ lớp kiểm thử, hãy truyền ComposeUiTestConfig đến 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
    }
}

Để biết các vấn đề khác về việc di chuyển và cách giải quyết, hãy xem bài viết Các lỗi thường gặp và cách khắc phục.

Di chuyển sang API kiểm thử phiên bản 2

Khi nâng cấp lên API phiên bản 2, bạn thường có thể sử dụng tính năng Tìm + Thay thế để cập nhật các gói nhập và áp dụng các thay đổi mới về trình điều phối.

Ngoài ra, bạn có thể yêu cầu Gemini di chuyển sang phiên bản 2 của API kiểm thử Compose bằng câu lệnh sau:

Di chuyển từ API kiểm thử phiên bản 1 sang API kiểm thử phiên bản 2

Câu lệnh này sẽ sử dụng hướng dẫn này để di chuyển sang API kiểm thử phiên bản 2.

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

Sử dụng câu lệnh AI

Câu lệnh AI được dùng trong Gemini trong Android Studio.

Tìm hiểu thêm về Gemini trong Studio tại đây: https://developer.android.com/studio/gemini/overview

Hãy sử dụng bảng sau để liên kết các API v1 không dùng nữa với các API thay thế v2:

Không dùng nữa (phiên bản 1)

Thay thế (phiên bản 2)

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

Khả năng tương thích ngược và các trường hợp ngoại lệ

Các API phiên bản 1 hiện tại hiện không còn được dùng nữa, nhưng hãy tiếp tục sử dụng UnconfinedTestDispatcher để duy trì hành vi hiện có và ngăn chặn các thay đổi có thể gây lỗi.

Sau đây là trường hợp ngoại lệ duy nhất mà hành vi mặc định đã thay đổi:

Trình điều phối kiểm thử mặc định dùng để chạy thành phần trong lớp AndroidComposeUiTestEnvironment đã chuyển từ UnconfinedTestDispatcher sang StandardTestDispatcher. Điều này ảnh hưởng đến các trường hợp mà bạn tạo một thực thể bằng hàm khởi tạo hoặc lớp con AndroidComposeUiTestEnvironment và gọi hàm khởi tạo đó.

Thay đổi chính: Tác động đến việc thực thi coroutine

Sự khác biệt chính giữa phiên bản 1 và phiên bản 2 của API là cách điều phối coroutine:

  • API phiên bản 1 (UnconfinedTestDispatcher): Khi một coroutine được khởi chạy, coroutine đó sẽ thực thi ngay lập tức trên luồng hiện tại, thường kết thúc trước khi dòng mã kiểm thử tiếp theo chạy. Không giống như hành vi trong môi trường thực tế, việc thực thi ngay lập tức này có thể vô tình che giấu các vấn đề về thời gian thực hoặc tình huống tương tranh xảy ra trong ứng dụng thực tế.
  • API phiên bản 2 (StandardTestDispatcher): Khi một coroutine được khởi chạy, coroutine đó sẽ được đưa vào hàng đợi và không thực thi cho đến khi kiểm thử chuyển đồng hồ ảo sang bước tiếp theo một cách rõ ràng. Các API kiểm thử Compose tiêu chuẩn (chẳng hạn như waitForIdle()) đã xử lý quá trình đồng bộ hoá này, vì vậy, hầu hết các kiểm thử dựa trên các API tiêu chuẩn này sẽ tiếp tục hoạt động mà không có thay đổi nào.

Các lỗi thường gặp và cách khắc phục

Nếu các thử nghiệm của bạn không thành công sau khi nâng cấp lên phiên bản 2, thì có thể các thử nghiệm đó có mẫu sau:

  • Lỗi: Bạn khởi chạy một tác vụ (ví dụ: ViewModel tải dữ liệu), nhưng câu nhận định của bạn không thành công ngay lập tức vì dữ liệu vẫn ở trạng thái "Đang tải".
  • Nguyên nhân: Với API phiên bản 2, coroutine được đưa vào hàng đợi thay vì thực thi ngay lập tức. Tác vụ đã được đưa vào hàng đợi nhưng chưa bao giờ thực sự chạy trước khi kết quả được kiểm tra.
  • Cách khắc phục: Tăng thời gian một cách rõ ràng. Bạn phải cho trình điều phối phiên bản 2 biết rõ thời điểm thực thi công việc.

Cách tiếp cận trước đây

Trong phiên bản 1, tác vụ được khởi chạy và hoàn tất ngay lập tức. Trong phiên bản 2, mã sau đây không thành công vì loadData() chưa thực sự chạy.

// 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)

Sử dụng waitForIdle hoặc runOnIdle để thực thi các tác vụ trong hàng đợi trước khi xác nhận.

Cách 1: Việc sử dụng waitForIdle sẽ đẩy nhanh đồng hồ cho đến khi giao diện người dùng ở trạng thái rảnh, xác minh rằng coroutine đã chạy.

viewModel.loadData()

// Explicitly run all queued tasks
composeTestRule.waitForIdle()

assertEquals(Success, viewModel.state.value)

Cách 2: Sử dụng runOnIdle để thực thi khối mã trên luồng giao diện người dùng sau khi giao diện người dùng ở trạng thái rảnh.

viewModel.loadData()

// Run the assertion after the UI is idle
composeTestRule.runOnIdle {
    assertEquals(Success, viewModel.state.value)
}

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

Trong các trường hợp liên quan đến việc đồng bộ hoá thủ công, chẳng hạn như khi tính năng tự động chuyển tiếp bị tắt, việc khởi chạy coroutine sẽ không dẫn đến việc thực thi ngay lập tức vì đồng hồ kiểm thử bị tạm dừng. Để thực thi coroutine trong hàng đợi mà không cần tăng đồng hồ ảo, hãy sử dụng API runCurrent(). Thao tác này sẽ chạy các tác vụ được lên lịch cho thời gian ảo hiện tại.

composeTestRule.mainClock.scheduler.runCurrent()

Trái ngược với waitForIdle() (đẩy nhanh đồng hồ kiểm thử cho đến khi giao diện người dùng ổn định), runCurrent() thực thi các tác vụ đang chờ xử lý trong khi vẫn duy trì thời gian ảo hiện tại. Hành vi này cho phép xác minh các trạng thái trung gian mà nếu không thì sẽ bị bỏ qua nếu đồng hồ được chuyển sang trạng thái rảnh.

Trình lập lịch biểu kiểm thử cơ bản dùng trong môi trường thử nghiệm sẽ được hiển thị. Bạn có thể sử dụng trình lập lịch biểu này cùng với API runTest của Kotlin để đồng bộ hoá đồng hồ kiểm thử.

Di chuyển sang runComposeUiTest

Nếu bạn đang sử dụng API kiểm thử Compose cùng với API runTest của Kotlin, bạn nên chuyển sang runComposeUiTest.

Cách tiếp cận trước đây

Việc sử dụng createComposeRule cùng với runTest sẽ tạo ra hai đồng hồ riêng biệt: một cho Compose và một cho phạm vi coroutine kiểm thử. Cấu hình này có thể buộc bạn phải đồng bộ hoá trình lập lịch kiểm thử theo cách thủ công.

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

API runComposeUiTest tự động thực thi khối kiểm thử trong phạm vi runTest của riêng API đó. Đồng hồ kiểm thử được đồng bộ hoá với môi trường Compose, vì vậy, bạn không cần phải quản lý trình lập lịch biểu theo cách thủ công nữa.

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