@@ -146,18 +146,140 @@ No action on your part is required, other than replacing `runBlocking` with `run
146146
147147By now, calls to ` pauseDispatcher ` and ` resumeDispatcher ` should be purged from the code base, so only the unpaused
148148variant 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:
150150code 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
154178any 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
160249at 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
324446The 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