@@ -146,18 +146,140 @@ No action on your part is required, other than replacing `runBlocking` with `run
146
146
147
147
By now, calls to ` pauseDispatcher ` and ` resumeDispatcher ` should be purged from the code base, so only the unpaused
148
148
variant of ` TestCoroutineDispatcher ` should be used.
149
- This version of the dispatcher, which can be observed has the property of eagerly entering ` launch ` and ` async ` blocks:
149
+ This version of the dispatcher has the property of eagerly entering ` launch ` and ` async ` blocks:
150
150
code until the first suspension is executed without dispatching.
151
151
152
- We ensured sure that, when run with an ` UnconfinedTestDispatcher ` , ` runTest ` also eagerly enters ` launch ` and ` async `
153
- blocks, but * this only works at the top level* : if a child coroutine also called ` launch ` or ` async ` , we don't provide
152
+ There are two common ways in which this property is useful.
153
+
154
+ #### ` TestCoroutineDispatcher ` for the top-level coroutine
155
+
156
+ Some tests that rely on ` launch ` and ` async ` blocks being entered immediately have a form similar to this:
157
+ ``` kotlin
158
+ runTest(TestCoroutineDispatcher ()) {
159
+ launch {
160
+ updateSomething()
161
+ }
162
+ checkThatSomethingWasUpdated()
163
+ launch {
164
+ updateSomethingElse()
165
+ }
166
+ checkThatSomethingElseWasUpdated()
167
+ }
168
+ ```
169
+
170
+ If the ` TestCoroutineDispatcher() ` is simply removed, ` StandardTestDispatcher() ` will be used, which will cause
171
+ the test to fail.
172
+
173
+ In these cases, ` UnconfinedTestDispatcher() ` should be used.
174
+ We ensured that, when run with an ` UnconfinedTestDispatcher ` , ` runTest ` also eagerly enters ` launch ` and ` async `
175
+ blocks.
176
+
177
+ Note though that * this only works at the top level* : if a child coroutine also called ` launch ` or ` async ` , we don't provide
154
178
any guarantees about their dispatching order.
155
179
156
- So, using ` UnconfinedTestDispatcher ` as an argument to ` runTest ` will probably lead to the test being executed as it
157
- did, but in the possible case that the test relies on the specific dispatching order of ` TestCoroutineDispatcher ` , it
158
- will need to be tweaked.
159
- If the test expects some code to have run at some point, but it hasn't, use ` runCurrent ` to force the tasks scheduled
180
+ #### ` TestCoroutineDispatcher ` for testing intermediate emissions
181
+
182
+ Some code tests ` StateFlow ` or channels in a manner similar to this:
183
+
184
+ ``` kotlin
185
+ @Test
186
+ fun testAllEmissions () = runTest(TestCoroutineDispatcher ()) {
187
+ val values = mutableListOf<Int >()
188
+ val stateFlow = MutableStateFlow (0 )
189
+ val job = launch {
190
+ stateFlow.collect {
191
+ values.add(it)
192
+ }
193
+ }
194
+ stateFlow.value = 1
195
+ stateFlow.value = 2
196
+ stateFlow.value = 3
197
+ job.cancel()
198
+ // each assignment will immediately resume the collecting child coroutine,
199
+ // so no values will be skipped.
200
+ assertEquals(listOf (0 , 1 , 2 , 3 ), values)
201
+ }
202
+ ```
203
+
204
+ Such code will fail when ` TestCoroutineDispatcher() ` is not used: not every emission will be listed.
205
+ In this particular case, none will be listed at all.
206
+
207
+ The reason for this is that setting ` stateFlow.value ` (as is sending to a channel, as are some other things) wakes up
208
+ the coroutine waiting for the new value, but * typically* does not immediately run the collecting code, instead simply
209
+ dispatching it.
210
+ The exceptions are the coroutines running in dispatchers that don't (always) go through a dispatch,
211
+ ` Dispatchers.Unconfined ` , ` Dispatchers.Main.immediate ` , ` UnconfinedTestDispatcher ` , or ` TestCoroutineDispatcher ` in
212
+ the unpaused state.
213
+
214
+ Therefore, a solution is to launch the collection in an unconfined dispatcher:
215
+
216
+ ``` kotlin
217
+ @Test
218
+ fun testAllEmissions () = runTest {
219
+ val values = mutableListOf<Int >()
220
+ val stateFlow = MutableStateFlow (0 )
221
+ val job = launch(UnconfinedTestDispatcher (testScheduler)) { // <------
222
+ stateFlow.collect {
223
+ values.add(it)
224
+ }
225
+ }
226
+ stateFlow.value = 1
227
+ stateFlow.value = 2
228
+ stateFlow.value = 3
229
+ job.cancel()
230
+ // each assignment will immediately resume the collecting child coroutine,
231
+ // so no values will be skipped.
232
+ assertEquals(listOf (0 , 1 , 2 , 3 ), values)
233
+ }
234
+ ```
235
+
236
+ Note that ` testScheduler ` is passed so that the unconfined dispatcher is linked to ` runTest ` .
237
+ Also, note that ` UnconfinedTestDispatcher ` is not passed to ` runTest ` .
238
+ This is due to the fact that, * inside* the ` UnconfinedTestDispatcher ` , there are no execution order guarantees,
239
+ so it would not be guaranteed that setting ` stateFlow.value ` would immediately run the collecting code
240
+ (though in this case, it does).
241
+
242
+ #### Other considerations
243
+
244
+ Using ` UnconfinedTestDispatcher ` as an argument to ` runTest ` will probably lead to the test being executed as it
245
+ did, but it's still possible that the test relies on the specific dispatching order of ` TestCoroutineDispatcher ` ,
246
+ so it will need to be tweaked.
247
+
248
+ If some code is expected to have run at some point, but it hasn't, use ` runCurrent ` to force the tasks scheduled
160
249
at this moment of time to run.
250
+ For example, the ` StateFlow ` example above can also be forced to succeed by doing this:
251
+
252
+ ``` kotlin
253
+ @Test
254
+ fun testAllEmissions () = runTest {
255
+ val values = mutableListOf<Int >()
256
+ val stateFlow = MutableStateFlow (0 )
257
+ val job = launch {
258
+ stateFlow.collect {
259
+ values.add(it)
260
+ }
261
+ }
262
+ runCurrent()
263
+ stateFlow.value = 1
264
+ runCurrent()
265
+ stateFlow.value = 2
266
+ runCurrent()
267
+ stateFlow.value = 3
268
+ runCurrent()
269
+ job.cancel()
270
+ // each assignment will immediately resume the collecting child coroutine,
271
+ // so no values will be skipped.
272
+ assertEquals(listOf (0 , 1 , 2 , 3 ), values)
273
+ }
274
+ ```
275
+
276
+ Be wary though of this approach: using ` runCurrent ` , ` advanceTimeBy ` , or ` advanceUntilIdle ` is, essentially,
277
+ simulating some particular execution order, which is not guaranteed to happen in production code.
278
+ For example, using ` UnconfinedTestDispatcher ` to fix this test reflects how, in production code, one could use
279
+ ` Dispatchers.Unconfined ` to observe all emitted values without conflation, but the ` runCurrent() ` approach only
280
+ states that the behavior would be observed if a dispatch were to happen at some chosen points.
281
+ It is, therefore, recommended to structure tests in a way that does not rely on a particular interleaving, unless
282
+ that is the intention.
161
283
162
284
### The job hierarchy is completely different.
163
285
@@ -322,4 +444,4 @@ fun testFoo() = runTest {
322
444
```
323
445
324
446
The reason this works is that all entities that depend on ` TestCoroutineScheduler ` will attempt to acquire one from
325
- the current ` Dispatchers.Main ` .
447
+ the current ` Dispatchers.Main ` .
0 commit comments