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ọnTestDispatcher
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
: StandardTestDispatcher
và UnconfinedTestDispatcher
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 các ví dụ này, chúng ta sẽ chèn CoroutineDispatcher
, nhưng bạn cũng có thể
chèn quảng cáo rộng hơn
CoroutineContext
loại, cho phép linh hoạt hơn trong quá trình thử nghiệm.
Đố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.setMain
và Dispatchers.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 ExampleRepository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ } class RepositoryTestWithRule { private val repository = ExampleRepository(/* 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 = ExampleRepository(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ả runTest
và TestDispatchers
đượ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 = ExampleRepository(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.