Kiểm thử coroutine của Kotlin trên Android

Bạn phải chú ý hơn đối với đoạn mã kiểm thử đơn vị dùng coroutine, vì quá trình thực thi có thể không đồng bộ và xảy ra trên nhiều luồng. Hướng dẫn này trình bày cách kiểm thử hàm tạm ngưng, các cấu trúc kiểm thử bạn cần phải làm quen và cách làm cho đoạn mã sử dụng coroutine có thể kiểm thử được.

Các API được dùng trong hướng dẫn này đều thuộc thư viện kotlinx.coroutines.test. Hãy nhớ thêm cấu phần phần mềm làm phần phụ thuộc kiểm thử cho dự án để có quyền truy cập vào các API này.

dependencies {
    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
}

Gọi hàm tạm ngưng trong kiểm thử

Để gọi các hàm tạm ngưng trong khi kiểm thử, bạn cần phải ở trong coroutine. Vì bản thân hàm kiểm thử JUnit không phải là hàm tạm ngưng, nên bạn cần gọi một trình tạo coroutine bên trong quy trình kiểm thử để bắt đầu một coroutine mới.

runTest là một trình tạo coroutine được thiết kế để kiểm thử. Dùng phương thức này để gói bất kỳ kiểm thử nào bao gồm coroutine. Vui lòng lưu ý coroutine không chỉ bắt đầu trực tiếp trong phần nội dung kiểm thử mà còn do các đối tượng dùng trong kiểm thử.

suspend fun fetchData(): String {
    delay(1000L)
    return "Hello world"
}

@Test
fun dataShouldBeHelloWorld() = runTest {
    val data = fetchData()
    assertEquals("Hello world", data)
}

Nói chung, bạn phải có một lệnh gọi runTest cho mỗi bài kiểm thử và nên dùng một nội dung biểu thức.

Việc gói mã kiểm thử trong runTest sẽ phát huy tác dụng khi kiểm thử các hàm tạm ngưng cơ bản và sẽ tự động bỏ qua mọi độ trễ trong coroutine, giúp bài kiểm thử ở trên hoàn tất nhanh hơn một giây.

Tuy nhiên, cần xem xét thêm một vài yếu tố, tuỳ thuộc vào tình huống xảy ra trong mã đang được kiểm thử:

  • Khi mã tạo coroutine mới ngoài coroutine kiểm thử cấp cao nhất mà runTest tạo, bạn sẽ cần kiểm soát cách các coroutine mới được lên lịch biểu bằng cách chọn TestDispatcher thích hợp.
  • Nếu mã của bạn chuyển quá trình thực thi coroutine sang trình điều phối khác (ví dụ: bằng cách sử dụng withContext), thì runTest thường vẫn hoạt động nhưng độ trễ sẽ không bị bỏ qua nữa và các hoạt động kiểm thử sẽ khó dự đoán hơn khi mã chạy trên nhiều chuỗi. Vì lý do này, bạn nên chèn trình điều phối kiểm thử để thay thế trình điều phối thực.

Bộ điều phối kiểm thử

TestDispatchers là các phương thức triển khai CoroutineDispatcher cho mục đích kiểm thử. Bạn cần phải sử dụng TestDispatchers nếu các coroutine mới được tạo trong quá trình kiểm thử để có thể dự đoán các coroutine mới.

Có hai cách triển khai TestDispatcher: StandardTestDispatcherUnconfinedTestDispatcher hai chức năng này thực hiện lịch biểu khác nhau đối với những coroutine mới bắt đầu. Cả hai đều sử dụng TestCoroutineScheduler để kiểm soát thời gian ảo và quản lý các coroutine đang chạy trong một kiểm thử.

Chỉ nên có một thực thể trình lập lịch biểu trong kiểm thử, dùng chung giữa tất cả TestDispatchers. Xem phần Chèn bộ điều phối kiểm thử để tìm hiểu về cách chia sẻ trình lập lịch biểu.

Để bắt đầu coroutine kiểm thử cấp cao nhất, runTest sẽ tạo một TestScope, là hoạt động triển khai CoroutineScope và sẽ luôn sử dụng TestDispatcher. Nếu không được chỉ định, TestScope sẽ tạo một StandardTestDispatcher theo mặc định và sử dụng giá trị này để chạy coroutine kiểm thử cấp cao nhất.

runTest theo dõi các coroutine đã được đưa vào hàng đợi trên trình lập lịch biểu được sử dụng bởi bộ điều phối của TestScope và sẽ không trả về nếu nội dung đó có tác vụ đang chờ xử lý trên trình lập lịch biểu đó.

Bộ điều phối kiểm thử tiêu chuẩn

Khi bạn bắt đầu coroutine mới trên StandardTestDispatcher, chúng sẽ được đưa vào hàng đợi trên trình lập lịch biểu cơ bản để chạy bất cứ khi nào luồng kiểm thử sẵn sàng sử dụng. Để cho phép các coroutine mới này chạy, bạn cần phải tạo chuỗi kiểm thử (giải phóng nó để các coroutine khác sử dụng). Hành vi xếp hàng này cho phép bạn kiểm soát chính xác cách các coroutine mới chạy trong quá trình kiểm thử, và cũng giống như việc lập lịch biểu của các coroutine trong mã sản xuất.

Nếu luồng kiểm thử không bao giờ được tạo ra trong quá trình thực thi coroutine kiểm thử cấp cao nhất, thì mọi coroutine mới sẽ chỉ chạy sau khi coroutine kiểm thử hoàn tất (nhưng trước khi runTest trả về):

@Test
fun standardTest() = runTest {
    val userRepo = UserRepository()

    launch { userRepo.register("Alice") }
    launch { userRepo.register("Bob") }

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails
}

Có một số cách tạo coroutine kiểm thử để các coroutine trong danh sách chờ chạy. Tất cả các lệnh gọi này cho phép coroutine khác chạy trên luồng kiểm thử trước khi quay lại:

  • advanceUntilIdle: Chạy tất cả các coroutine khác trên trình lập lịch biểu cho đến khi không còn gì trong hàng đợi. Đây là một lựa chọn mặc định tốt để chạy tất cả coroutine đang chờ xử lý và nó sẽ hoạt động trong hầu hết các trường hợp kiểm thử.
  • advanceTimeBy: Đẩy nhanh thời gian ảo theo khoảng cho trước và chạy mọi coroutine được lên lịch biểu trước khoảng đó trong thời gian ảo.
  • runCurrent: Chạy coroutine được lên lịch biểu vào thời gian ảo hiện tại.

Để khắc phục kiểm thử trước đó, bạn có thể dùng advanceUntilIdle để cho phép hai coroutine đang chờ xử lý thực hiện nhiệm vụ của nó trước khi tiếp tục xác nhận:

@Test
fun standardTest() = runTest {
    val userRepo = UserRepository()

    launch { userRepo.register("Alice") }
    launch { userRepo.register("Bob") }
    advanceUntilIdle() // Yields to perform the registrations

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
}

Bộ điều phối kiểm thử tự do

Khi coroutine mới được bắt đầu trên UnconfinedTestDispatcher, chúng sẽ bắt đầu một cách mượt mà trên luồng hiện tại. Điều này có nghĩa là các biến sẽ bắt đầu chạy ngay lập tức mà không cần chờ trình tạo coroutine trả về. Trong nhiều trường hợp, hành vi điều phối này làm cho mã kiểm thử đơn giản hơn, vì bạn không cần phải tạo luồng kiểm thử theo cách thủ công để cho phép coroutine mới chạy.

Tuy nhiên, hành vi này khác với những gì bạn sẽ thấy trong phiên bản chính thức với bộ điều phối không kiểm thử. Hãy ưu tiên sử dụng StandardTestDispatcher nếu kiểm thử của bạn tập trung vào tính đồng thời.

Để sử dụng trình điều phối này cho coroutine kiểm thử cấp cao nhất trong runTest thay vì trình mặc định, hãy tạo một thực thể và truyền thực thể đó dưới dạng tham số. Việc này giúp các coroutine mới được tạo trong runTest thực thi một cách nghiêm ngặt vì chúng kế thừa bộ điều phối từ TestScope.

@Test
fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) {
    val userRepo = UserRepository()

    launch { userRepo.register("Alice") }
    launch { userRepo.register("Bob") }

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
}

Ở ví dụ này, các lệnh gọi khởi chạy sẽ bắt đầu coroutine mới một cách mượt mà trên UnconfinedTestDispatcher, tức là mỗi lệnh gọi để khởi chạy sẽ chỉ trả về sau khi quá trình đăng ký hoàn tất.

Hãy nhớ UnconfinedTestDispatcher chạy coroutine mới ngay lập tức, nhưng không có nghĩa là điều này khiến hoạt động chạy hoàn tất nhanh chóng. Nếu coroutine mới tạm ngưng, các coroutine khác sẽ tiếp tục thực thi.

Ví dụ: coroutine mới được chạy trong kiểm thử này sẽ đăng ký Alice, nhưng sau đó coroutine sẽ tạm ngưng khi delay được gọi. Việc này cho phép coroutine cấp cao nhất tiếp tục xác nhận và kiểm thử sẽ không thành công khi Bob chưa được đăng ký:

@Test
fun yieldingTest() = runTest(UnconfinedTestDispatcher()) {
    val userRepo = UserRepository()

    launch {
        userRepo.register("Alice")
        delay(10L)
        userRepo.register("Bob")
    }

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails
}

Chèn bộ điều phối kiểm thử

Mã đang được kiểm thử có thể dùng trình điều phối để chuyển đổi luồng (sử dụng withContext) hoặc để bắt đầu coroutine mới. Khi mã được thực thi trên nhiều luồng song song, các kiểm thử có thể trở nên không ổn định. Bạn có thể sẽ gặp khó khăn khi cần thực hiện câu nhận định đúng thời điểm hoặc để hoàn thành các thao tác nếu những thao tác đó đang chạy trên các luồng nền mà bạn không có quyền kiểm soát.

Trong kiểm thử, hãy thay thế những bộ điều phối này bằng các thực thể của TestDispatchers. Việc này có một số lợi ích:

  • Mã này sẽ chạy trên một luồng kiểm thử đơn, làm cho các kiểm thử có tính quyết định hơn
  • Bạn có thể kiểm soát cách các coroutine mới được lập lịch biểu và thực thi
  • TestDispatchers sử dụng trình lập lịch biểu cho thời gian ảo, giúp tự động bỏ qua thời gian chờ và cho phép đẩy nhanh thời gian theo cách thủ công

Việc sử dụng tính năng chèn phần phụ thuộc để cung cấp trình điều phối cho các lớp giúp bạn dễ dàng thay thế trình điều phối thực trong bài kiểm thử. Trong những ví dụ này, chúng tôi sẽ chèn một CoroutineDispatcher, nhưng bạn cũng có thể chèn loại CoroutineContext rộng hơn. Đây là loại cho phép linh hoạt hơn nữa trong quá trình kiểm thử.

Đối với các lớp bắt đầu coroutine, bạn cũng có thể chèn CoroutineScope thay vì trình điều phối, như đã nêu chi tiết trong phần Chèn một phạm vi.

Theo mặc định, TestDispatchers sẽ tạo một trình lập lịch biểu mới khi chúng được tạo bản sao. Bên trong runTest, bạn có thể truy cập vào thuộc tính testScheduler của TestScope và chuyển thuộc tính đó vào bất kỳ TestDispatchers nào mới được tạo. Thao tác này sẽ chia sẻ sự hiểu biết của nó về thời gian ảo và các phương thức như advanceUntilIdle sẽ chạy coroutine trên mọi trình điều phối kiểm thử để hoàn tất.

Ở ví dụ sau, bạn có thể thấy lớp Repository tạo một coroutine mới bằng cách sử dụng trình điều phối IO trong phương thức initialize và chuyển phương thức gọi sang trình điều phối IO trong phương thức fetchData:

// Example class demonstrating dispatcher use cases
class Repository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
    private val scope = CoroutineScope(ioDispatcher)
    val initialized = AtomicBoolean(false)

    // A function that starts a new coroutine on the IO dispatcher
    fun initialize() {
        scope.launch {
            initialized.set(true)
        }
    }

    // A suspending function that switches to the IO dispatcher
    suspend fun fetchData(): String = withContext(ioDispatcher) {
        require(initialized.get()) { "Repository should be initialized first" }
        delay(500L)
        "Hello world"
    }
}

Trong kiểm thử, bạn có thể chèn TestDispatcher triển khai để thay thế bộ điều phối IO.

Trong ví dụ bên dưới, chúng ta chèn StandardTestDispatcher vào kho lưu trữ và sử dụng advanceUntilIdle để đảm bảo coroutine mới bắt đầu trong initialize hoàn tất trước khi tiếp tục.

fetchData cũng sẽ được hưởng lợi từ việc chạy trên TestDispatcher, vì nó sẽ chạy trên luồng kiểm thử và bỏ qua độ trễ trong quá trình kiểm thử.

class RepositoryTest {
    @Test
    fun repoInitWorksAndDataIsHelloWorld() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val repository = Repository(dispatcher)

        repository.initialize()
        advanceUntilIdle() // Runs the new coroutine
        assertEquals(true, repository.initialized.get())

        val data = repository.fetchData() // No thread switch, delay is skipped
        assertEquals("Hello world", data)
    }
}

Bạn có thể nâng cao các coroutine mới bắt đầu trong TestDispatcher theo cách thủ công như minh hoạ ở trên bằng initialize. Tuy nhiên, cần lưu ý điều này là không thể hoặc không mong muốn trong mã sản xuất. Thay vào đó, phương thức này phải được thiết kế lại để tạm ngưng (để thực thi theo tuần tự) hoặc trả về một giá trị Deferred (để thực thi đồng thời).

Ví dụ: bạn có thể dùng async để bắt đầu một coroutine mới và tạo một Deferred:

class BetterRepository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
    private val scope = CoroutineScope(ioDispatcher)

    fun initialize() = scope.async {
        // ...
    }
}

Cách này giúp bạn await có thể hoàn tất mã này một cách an toàn trong cả mã kiểm thử và sản xuất:

@Test
fun repoInitWorks() = runTest {
    val dispatcher = StandardTestDispatcher(testScheduler)
    val repository = BetterRepository(dispatcher)

    repository.initialize().await() // Suspends until the new coroutine is done
    assertEquals(true, repository.initialized.get())
    // ...
}

runTest sẽ đợi các coroutine đang chờ hoàn tất trước khi quay lại nếu coroutine trên TestDispatcher mà hệ thống này dùng chung trình lập lịch biểu. Nó cũng sẽ chờ các coroutine là phần tử con của coroutine kiểm thử cấp cao nhất, ngay cả khi chúng thuộc trình điều phối khác (tối đa là thời gian chờ được chỉ định theo tham số dispatchTimeoutMs, mặc định là 60 giây).

Đặt bộ điều phối Chính

Trong các kiểm thử đơn vị cục bộ, bộ điều phối Main gói luồng giao diện người dùng Android sẽ không khả dụng, vì những kiểm thử này được thực thi trên một máy JVM cục bộ chứ không phải một thiết bị Android. Nếu mã của bạn đang được kiểm thử tham chiếu chuỗi chính, thì mã sẽ gửi một ngoại lệ trong quá trình kiểm thử đơn vị.

Trong một số trường hợp, bạn có thể chèn trình điều phối Main theo cách chèn các trình điều phối khác, như mô tả trong phần trước, cho phép bạn thay thế bằng TestDispatcher trong bài kiểm thử. Tuy nhiên, về bản chất, một số API như viewModelScope sẽ dùng trình điều phối Main được mã hoá cứng.

Dưới đây là ví dụ về cách triển khai ViewModel sử dụng viewModelScope để chạy một coroutine tải dữ liệu:

class HomeViewModel : ViewModel() {
    private val _message = MutableStateFlow("")
    val message: StateFlow<String> get() = _message

    fun loadMessage() {
        viewModelScope.launch {
            _message.value = "Greetings!"
        }
    }
}

Để thay thế trình điều phối Main bằng TestDispatcher trong mọi trường hợp, hãy sử dụng các hàm Dispatchers.setMainDispatchers.resetMain.

class HomeViewModelTest {
    @Test
    fun settingMainDispatcher() = runTest {
        val testDispatcher = UnconfinedTestDispatcher(testScheduler)
        Dispatchers.setMain(testDispatcher)

        try {
            val viewModel = HomeViewModel()
            viewModel.loadMessage() // Uses testDispatcher, runs its coroutine eagerly
            assertEquals("Greetings!", viewModel.message.value)
        } finally {
            Dispatchers.resetMain()
        }
    }
}

Nếu trình điều phối Main đã được thay thế bằng TestDispatcher, thì mọi TestDispatchers mới tạo sẽ tự động sử dụng trình lập lịch biểu của trình điều phối Main, bao gồm cả StandardTestDispatcher do runTest tạo trong trường hợp không có trình điều phối nào khác được truyền vào đó.

Nhờ vậy, bạn có thể dễ dàng đảm bảo chỉ có một trình lập lịch biểu được sử dụng trong quá trình kiểm thử. Để chế độ này hoạt động, bạn hãy nhớ tạo tất cả các bản sao TestDispatcher khác sau khi gọi Dispatchers.setMain.

Một mẫu chung để tránh lặp lại mã sẽ thay thế trình điều phối Main trong mỗi hoạt động kiểm thử là trích xuất mã đó vào quy tắc kiểm thử JUnit:

// Reusable JUnit4 TestRule to override the Main dispatcher
class MainDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

class HomeViewModelTestUsingRule {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun settingMainDispatcher() = runTest { // Uses Main’s scheduler
        val viewModel = HomeViewModel()
        viewModel.loadMessage()
        assertEquals("Greetings!", viewModel.message.value)
    }
}

Quá trình triển khai quy tắc này sử dụng UnconfinedTestDispatcher theo mặc định, nhưng hệ thống có thể chuyển StandardTestDispatcher dưới dạng thông số nếu bộ điều phối Main không thực thi một cách nhanh chóng trong lớp kiểm thử nhất định.

Khi cần một thực thể TestDispatcher trong phần nội dung kiểm thử, bạn có thể sử dụng lại testDispatcher của quy tắc, miễn rằng đó là loại mong muốn. Nếu muốn thông báo rõ ràng về loại TestDispatcher sử dụng trong quá trình kiểm thử hoặc nếu cần một TestDispatcher khác với loại dùng cho Main, thì bạn có thể tạo TestDispatcher mới trong runTest. Khi bộ điều phối Main được đặt thành TestDispatcher, mọi TestDispatchers mới tạo sẽ tự động chia sẻ trình lập lịch biểu.

class DispatcherTypesTest {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun injectingTestDispatchers() = runTest { // Uses Main’s scheduler
        // Use the UnconfinedTestDispatcher from the Main dispatcher
        val unconfinedRepo = Repository(mainDispatcherRule.testDispatcher)

        // Create a new StandardTestDispatcher (uses Main’s scheduler)
        val standardRepo = Repository(StandardTestDispatcher())
    }
}

Tạo bộ điều phối ngoài một kiểm thử

Trong một số trường hợp, bạn có thể cần phải có một TestDispatcher ngoài phương pháp kiểm thử. Ví dụ: trong quá trình khởi tạo thuộc tính trong lớp kiểm thử:

class Repository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ }

class RepositoryTestWithRule {
    private val repository = Repository(/* What TestDispatcher? */)

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun someRepositoryTest() = runTest {
        // Test the repository...
        // ...
    }
}

Nếu bạn đang thay thế trình điều phối Main như minh hoạ ở phần trước, thì TestDispatchers tạo sau khi trình điều phối Main được thay thế sẽ tự động chia sẻ trình lập lịch biểu của nó.

Tuy nhiên, TestDispatchers sẽ không được tạo dưới dạng thuộc tính của lớp kiểm thử hoặc TestDispatchers được tạo trong quá trình khởi tạo thuộc tính của lớp kiểm thử. Các toán tử này được khởi tạo trước khi bộ điều phối Main được thay thế. Do đó, chúng sẽ tạo các trình lập lịch biểu mới.

Để đảm bảo chỉ có một trình lập lịch biểu trong kiểm thử, trước tiên, hãy tạo thuộc tính MainDispatcherRule. Sau đó, hãy dùng lại trình điều phối (hoặc trình lập lịch biểu nếu bạn cần một TestDispatcher thuộc loại khác) trong trình khởi chạy của các thuộc tính cấp lớp khác khi cần.

class RepositoryTestWithRule {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    private val repository = Repository(mainDispatcherRule.testDispatcher)

    @Test
    fun someRepositoryTest() = runTest { // Takes scheduler from Main
        // Any TestDispatcher created here also takes the scheduler from Main
        val newTestDispatcher = StandardTestDispatcher()

        // Test the repository...
    }
}

Vui lòng lưu ý cả runTestTestDispatchers được tạo trong hoạt động kiểm thử này vẫn sẽ tự động chia sẻ trình lập lịch biểu của trình điều phối Main.

Nếu bạn không thay thế trình điều phối Main, hãy tạo TestDispatcher đầu tiên (trình lập lịch biểu mới) làm thuộc tính của lớp. Sau đó, truyền trình lập lịch biểu đó theo cách thủ công vào từng lệnh gọi runTest và mỗi TestDispatcher mới được tạo, cả dưới dạng thuộc tính lẫn trong bài kiểm thử:

class RepositoryTest {
    // Creates the single test scheduler
    private val testDispatcher = UnconfinedTestDispatcher()
    private val repository = Repository(testDispatcher)

    @Test
    fun someRepositoryTest() = runTest(testDispatcher.scheduler) {
        // Take the scheduler from the TestScope
        val newTestDispatcher = UnconfinedTestDispatcher(this.testScheduler)
        // Or take the scheduler from the first dispatcher, they’re the same
        val anotherTestDispatcher = UnconfinedTestDispatcher(testDispatcher.scheduler)

        // Test the repository...
    }
}

Ở ví dụ này, trình lập lịch biểu trong trình điều phối đầu tiên được chuyển cho runTest. Thao tác này sẽ tạo một StandardTestDispatcher mới cho TestScope bằng cách sử dụng trình lập lịch biểu đó. Bạn cũng có thể chuyển trực tiếp bộ điều phối cho runTest để chạy coroutine kiểm thử trên bộ điều phối đó.

Tạo phạm vi kiểm thử của riêng bạn

Giống như với TestDispatchers, bạn có thể cần phải truy cập vào TestScope ngoài nội dung kiểm thử. Mặc dù runTest sẽ tự động tạo một TestScope, nhưng bạn cũng có thể tạo TestScope của riêng mình để sử dụng với runTest.

Khi thực hiện việc này, hãy nhớ gọi runTest trên TestScope bạn đã tạo:

class SimpleExampleTest {
    val testScope = TestScope() // Creates a StandardTestDispatcher

    @Test
    fun someTest() = testScope.runTest {
        // ...
    }
}

Đoạn mã ở trên sẽ ngầm tạo một StandardTestDispatcher cho TestScope, cũng như một trình lập lịch biểu mới. Bạn cũng có thể tạo các đối tượng này một cách rõ ràng. Việc đó hữu ích nếu bạn cần tích hợp tính năng này với thiết lập chèn phần phụ thuộc.

class ExampleTest {
    val testScheduler = TestCoroutineScheduler()
    val testDispatcher = StandardTestDispatcher(testScheduler)
    val testScope = TestScope(testDispatcher)

    @Test
    fun someTest() = testScope.runTest {
        // ...
    }
}

Chèn một phạm vi

Nếu có một lớp tạo coroutine mà bạn cần kiểm soát trong quá trình kiểm thử, bạn có thể chèn một phạm vi coroutine vào lớp đó, thay thế phạm vi này bằng một TestScope trong quá trình kiểm thử.

Trong ví dụ sau, lớp UserState phụ thuộc vào một UserRepository để đăng ký người dùng mới và tìm nạp danh sách người dùng đã đăng ký. Khi những lệnh gọi này đến UserRepository đang tạm ngưng các lệnh gọi hàm, UserState sẽ dùng CoroutineScope đã chèn để bắt đầu một coroutine mới bên trong hàm registerUser.

class UserState(
    private val userRepository: UserRepository,
    private val scope: CoroutineScope,
) {
    private val _users = MutableStateFlow(emptyList<String>())
    val users: StateFlow<List<String>> = _users.asStateFlow()

    fun registerUser(name: String) {
        scope.launch {
            userRepository.register(name)
            _users.update { userRepository.getAllUsers() }
        }
    }
}

Để kiểm thử lớp này, bạn có thể chuyển vào TestScope từ runTest khi tạo đối tượng UserState:

class UserStateTest {
    @Test
    fun addUserTest() = runTest { // this: TestScope
        val repository = FakeUserRepository()
        val userState = UserState(repository, scope = this)

        userState.registerUser("Mona")
        advanceUntilIdle() // Let the coroutine complete and changes propagate

        assertEquals(listOf("Mona"), userState.users.value)
    }
}

Để chèn một phạm vi bên ngoài hàm kiểm thử (ví dụ: chèn vào một đối tượng kiểm thử được tạo dưới dạng thuộc tính trong lớp kiểm thử), hãy xem phần Tạo TestScope của riêng bạn.

Tài nguyên khác