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:
val testConfig = ComposeUiTestConfig( effectContext = EmptyCoroutineContext, runTestContext = EmptyCoroutineContext, testTimeout = 30.seconds ) @get:Rule val rule = createComposeRule(config = testConfig) // OR runComposeUiTest(config = testConfig) {}
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:
Câu lệnh cho AI
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.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) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
Phương pháp đề xuất
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() } }
Phương pháp đề xuất
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() } }