Skip to content

Commit 47e0a5e

Browse files
committed
Update the migration guide to highlight how to test all emissions
Addresses #3143 Addresses #3120
1 parent 5a71f7b commit 47e0a5e

File tree

1 file changed

+130
-8
lines changed

1 file changed

+130
-8
lines changed

kotlinx-coroutines-test/MIGRATION.md

+130-8
Original file line numberDiff line numberDiff line change
@@ -146,18 +146,140 @@ No action on your part is required, other than replacing `runBlocking` with `run
146146

147147
By now, calls to `pauseDispatcher` and `resumeDispatcher` should be purged from the code base, so only the unpaused
148148
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:
150150
code until the first suspension is executed without dispatching.
151151

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
154178
any guarantees about their dispatching order.
155179

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
160249
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.
161283

162284
### The job hierarchy is completely different.
163285

@@ -322,4 +444,4 @@ fun testFoo() = runTest {
322444
```
323445

324446
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

Comments
 (0)