xref: /aosp_15_r20/external/kotlinx.coroutines/kotlinx-coroutines-test/MIGRATION.md (revision 7a7160fed73afa6648ef8aa100d4a336fe921d9a)
1# Migration to the new kotlinx-coroutines-test API
2
3In version 1.6.0, the API of the test module changed significantly.
4This is a guide for gradually adapting the existing test code to the new API.
5This guide is written step-by-step; the idea is to separate the migration into several sets of small changes.
6
7## Remove custom `UncaughtExceptionCaptor`, `DelayController`, and `TestCoroutineScope` implementations
8
9We couldn't find any code that defined new implementations of these interfaces, so they are deprecated. It's likely that
10you don't need to do anything for this section.
11
12### `UncaughtExceptionCaptor`
13
14If the code base has an `UncaughtExceptionCaptor`, its special behavior as opposed to just `CoroutineExceptionHandler`
15was that, at the end of `runBlockingTest` or `cleanupTestCoroutines` (or both), its `cleanupTestCoroutines` procedure
16was called.
17
18We currently don't provide a replacement for this.
19However, `runTest` follows structured concurrency better than `runBlockingTest` did, so exceptions from child coroutines
20are propagated structurally, which makes uncaught exception handlers less useful.
21
22If you have a use case for this, please tell us about it at the issue tracker.
23Meanwhile, it should be possible to use a custom exception captor, which should only implement
24`CoroutineExceptionHandler` now, like this:
25
26```kotlin
27@Test
28fun testFoo() = runTest {
29    val customCaptor = MyUncaughtExceptionCaptor()
30    launch(customCaptor) {
31        // ...
32    }
33    advanceUntilIdle()
34    customCaptor.cleanupTestCoroutines()
35}
36```
37
38### `DelayController`
39
40We don't provide a way to define custom dispatching strategies that support virtual time.
41That said, we significantly enhanced this mechanism:
42* Using multiple test dispatchers simultaneously is supported.
43  For the dispatchers to have a shared knowledge of the virtual time, either the same `TestCoroutineScheduler` should be
44  passed to each of them, or all of them should be constructed after `Dispatchers.setMain` is called with some test
45  dispatcher.
46* Both a simple `StandardTestDispatcher` that is always paused, and unconfined `UnconfinedTestDispatcher` are provided.
47
48If you have a use case for `DelayController` that's not covered by what we provide, please tell us about it in the issue
49tracker.
50
51### `TestCoroutineScope`
52
53This scope couldn't be meaningfully used in tandem with `runBlockingTest`: according to the definition of
54`TestCoroutineScope.runBlockingTest`, only the scope's `coroutineContext` is used.
55So, there could be two reasons for defining a custom implementation:
56
57* Avoiding the restrictions on placed `coroutineContext` in the `TestCoroutineScope` constructor function.
58  These restrictions consisted of requirements for `CoroutineExceptionHandler` being an `UncaughtExceptionCaptor`, and
59  `ContinuationInterceptor` being a `DelayController`, so it is also possible to fulfill these restrictions by defining
60  conforming instances. In this case, follow the instructions about replacing them.
61* Using without `runBlockingTest`. In this case, you don't even need to implement `TestCoroutineScope`: nothing else
62  accepts a `TestCoroutineScope` specifically as an argument.
63
64## Remove usages of `TestCoroutineExceptionHandler` and `TestCoroutineScope.uncaughtExceptions`
65
66It is already illegal to use a `TestCoroutineScope` without performing `cleanupTestCoroutines`, so the valid uses of
67`TestCoroutineExceptionHandler` include:
68
69* Accessing `uncaughtExceptions` in the middle of the test to make sure that there weren't any uncaught exceptions
70  *yet*.
71  If there are any, they will be thrown by the cleanup procedure anyway.
72  We don't support this use case, given how comparatively rare it is, but it can be handled in the same way as the
73  following one.
74* Accessing `uncaughtExceptions` when the uncaught exceptions are actually expected.
75  In this case, `cleanupTestCoroutines` will fail with an exception that is being caught later.
76  It would be better in this case to use a custom `CoroutineExceptionHandler` so that actual problems that could be
77  found by the cleanup procedure are not superseded by the exceptions that are expected.
78  An example is shown below.
79
80```kotlin
81val exceptions = mutableListOf<Throwable>()
82val customCaptor = CoroutineExceptionHandler { ctx, throwable ->
83    exceptions.add(throwable) // add proper synchronization if the test is multithreaded
84}
85
86@Test
87fun testFoo() = runTest {
88    launch(customCaptor) {
89        // ...
90    }
91    advanceUntilIdle()
92    // check the list of the caught exceptions
93}
94```
95
96## Auto-replace `TestCoroutineScope` constructor function with `createTestCoroutineScope`
97
98This should not break anything, as `TestCoroutineScope` is now defined in terms of `createTestCoroutineScope`.
99If it does break something, it means that you already supplied a `TestCoroutineScheduler` to some scope; in this case,
100also pass this scheduler as the argument to the dispatcher.
101
102## Replace usages of `pauseDispatcher` and `resumeDispatcher` with a `StandardTestDispatcher`
103
104* In places where `pauseDispatcher` in its block form is called, replace it with a call to
105  `withContext(StandardTestDispatcher(testScheduler))`
106  (`testScheduler` is available as a field of `TestCoroutineScope`,
107  or `scheduler` is available as a field of `TestCoroutineDispatcher`),
108  followed by `advanceUntilIdle()`.
109  This is not an automatic replacement, as there can be tricky situations where the test dispatcher is already paused
110  when `pauseDispatcher { X }` is called. In such cases, simply replace `pauseDispatcher { X }` with `X`.
111* Often, `pauseDispatcher()` in a non-block form is used at the start of the test.
112  Then, attempt to remove `TestCoroutineDispatcher` from the arguments to `createTestCoroutineScope`,
113  if a standalone `TestCoroutineScope` or the `scope.runBlockingTest` form is used,
114  or pass a `StandardTestDispatcher` as an argument to `runBlockingTest`.
115  This will lead to the test using a `StandardTestDispatcher`, which does not allow pausing and resuming,
116  instead of the deprecated `TestCoroutineDispatcher`.
117* Sometimes, `pauseDispatcher()` and `resumeDispatcher()` are employed used throughout the test.
118  In this case, attempt to wrap everything until the next `resumeDispatcher()` in
119  a `withContext(StandardTestDispatcher(testScheduler))` block, or try using some other combinations of
120  `StandardTestDispatcher` (where dispatches are needed) and `UnconfinedTestDispatcher` (where it isn't important where
121  execution happens).
122
123## Replace `advanceTimeBy(n)` with `advanceTimeBy(n); runCurrent()`
124
125For `TestCoroutineScope` and `DelayController`, the `advanceTimeBy` method is deprecated.
126It is not deprecated for `TestCoroutineScheduler` and `TestScope`, but has a different meaning: it does not run the
127tasks scheduled *at* `currentTime + n`.
128
129There is an automatic replacement for this deprecation, which produces correct but inelegant code.
130
131Alternatively, you can wait until replacing `TestCoroutineScope` with `TestScope`: it's possible that you will not
132encounter this edge case.
133
134## Replace `runBlockingTest` with `runTest(UnconfinedTestDispatcher())`
135
136This is a major change, affecting many things, and can be done in parallel with replacing `TestCoroutineScope` with
137`TestScope`.
138
139Significant differences of `runTest` from `runBlockingTest` are each given a section below.
140
141### It works properly with other dispatchers and asynchronous completions.
142
143No action on your part is required, other than replacing `runBlocking` with `runTest` as well.
144
145### It uses `StandardTestDispatcher` by default, not `TestCoroutineDispatcher`.
146
147By now, calls to `pauseDispatcher` and `resumeDispatcher` should be purged from the code base, so only the unpaused
148variant of `TestCoroutineDispatcher` should be used.
149This version of the dispatcher has the property of eagerly entering `launch` and `async` blocks:
150code until the first suspension is executed without dispatching.
151
152There are two common ways in which this property is useful.
153
154#### `TestCoroutineDispatcher` for the top-level coroutine
155
156Some tests that rely on `launch` and `async` blocks being entered immediately have a form similar to this:
157```kotlin
158runTest(TestCoroutineDispatcher()) {
159    launch {
160        updateSomething()
161    }
162    checkThatSomethingWasUpdated()
163    launch {
164        updateSomethingElse()
165    }
166    checkThatSomethingElseWasUpdated()
167}
168```
169
170If the `TestCoroutineDispatcher()` is simply removed, `StandardTestDispatcher()` will be used, which will cause
171the test to fail.
172
173In these cases, `UnconfinedTestDispatcher()` should be used.
174We ensured that, when run with an `UnconfinedTestDispatcher`, `runTest` also eagerly enters `launch` and `async`
175blocks.
176
177Note though that *this only works at the top level*: if a child coroutine also called `launch` or `async`, we don't provide
178any guarantees about their dispatching order.
179
180#### `TestCoroutineDispatcher` for testing intermediate emissions
181
182Some code tests `StateFlow` or channels in a manner similar to this:
183
184```kotlin
185@Test
186fun 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
204Such code will fail when `TestCoroutineDispatcher()` is not used: not every emission will be listed.
205In this particular case, none will be listed at all.
206
207The reason for this is that setting `stateFlow.value` (as is sending to a channel, as are some other things) wakes up
208the coroutine waiting for the new value, but *typically* does not immediately run the collecting code, instead simply
209dispatching it.
210The 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
212the unpaused state.
213
214Therefore, a solution is to launch the collection in an unconfined dispatcher:
215
216```kotlin
217@Test
218fun 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
236Note that `testScheduler` is passed so that the unconfined dispatcher is linked to `runTest`.
237Also, note that `UnconfinedTestDispatcher` is not passed to `runTest`.
238This is due to the fact that, *inside* the `UnconfinedTestDispatcher`, there are no execution order guarantees,
239so 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
244Using `UnconfinedTestDispatcher` as an argument to `runTest` will probably lead to the test being executed as it
245did, but it's still possible that the test relies on the specific dispatching order of `TestCoroutineDispatcher`,
246so it will need to be tweaked.
247
248If some code is expected to have run at some point, but it hasn't, use `runCurrent` to force the tasks scheduled
249at this moment of time to run.
250For example, the `StateFlow` example above can also be forced to succeed by doing this:
251
252```kotlin
253@Test
254fun 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
276Be wary though of this approach: using `runCurrent`, `advanceTimeBy`, or `advanceUntilIdle` is, essentially,
277simulating some particular execution order, which is not guaranteed to happen in production code.
278For 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
280states that the behavior would be observed if a dispatch were to happen at some chosen points.
281It is, therefore, recommended to structure tests in a way that does not rely on a particular interleaving, unless
282that is the intention.
283
284### The job hierarchy is completely different.
285
286- Structured concurrency is used, with the scope provided as the receiver of `runTest` actually being the scope of the
287  created coroutine.
288- Not `SupervisorJob` but a normal `Job` is used for the `TestCoroutineScope`.
289- The job passed as an argument is used as a parent job.
290
291Most tests should not be affected by this. In case your test is, try explicitly launching a child coroutine with a
292`SupervisorJob`; this should make the job hierarchy resemble what it used to be.
293
294```kotlin
295@Test
296fun testFoo() = runTest {
297    val deferred = async(SupervisorJob()) {
298        // test code
299    }
300    advanceUntilIdle()
301    deferred.getCompletionExceptionOrNull()?.let {
302      throw it
303    }
304}
305```
306
307### Only a single call to `runTest` is permitted per test.
308
309In order to work on JS, only a single call to `runTest` must happen during one test, and its result must be returned
310immediately:
311
312```kotlin
313@Test
314fun testFoo(): TestResult {
315    // arbitrary code here
316    return runTest {
317        // ...
318    }
319}
320```
321
322When used only on the JVM, `runTest` will work when called repeatedly, but this is not supported.
323Please only call `runTest` once per test, and if for some reason you can't, please tell us about in on the issue
324tracker.
325
326### It uses `TestScope`, not `TestCoroutineScope`, by default.
327
328There is a `runTestWithLegacyScope` method that allows migrating from `runBlockingTest` to `runTest` before migrating
329from `TestCoroutineScope` to `TestScope`, if exactly the `TestCoroutineScope` needs to be passed somewhere else and
330`TestScope` will not suffice.
331
332## Replace `TestCoroutineScope.cleanupTestCoroutines` with `runTest`
333
334Likely can be done together with the next step.
335
336Remove all calls to `TestCoroutineScope.cleanupTestCoroutines` from the code base.
337Instead, as the last step of each test, do `return scope.runTest`; if possible, the whole test body should go inside
338the `runTest` block.
339
340The cleanup procedure in `runTest` will not check that the virtual time doesn't advance during cleanup.
341If a test must check that no other delays are remaining after it has finished, the following form may help:
342```kotlin
343runTest {
344    testBody()
345    val timeAfterTest = currentTime()
346    advanceUntilIdle() // run the remaining tasks
347    assertEquals(timeAfterTest, currentTime()) // will fail if there were tasks scheduled at a later moment
348}
349```
350Note that this will report time advancement even if the job scheduled at a later point was cancelled.
351
352It may be the case that `cleanupTestCoroutines` must be executed after de-initialization in `@AfterTest`, which happens
353outside the test itself.
354In this case, we propose that you write a wrapper of the form:
355
356```kotlin
357fun runTestAndCleanup(body: TestScope.() -> Unit) = runTest {
358    try {
359        body()
360    } finally {
361        // the usual cleanup procedures that used to happen before `cleanupTestCoroutines`
362    }
363}
364```
365
366## Replace `runBlockingTest` with `runBlockingTestOnTestScope`, `createTestCoroutineScope` with `TestScope`
367
368Also, replace `runTestWithLegacyScope` with just `runTest`.
369All of this can be done in parallel with replacing `runBlockingTest` with `runTest`.
370
371This step should remove all uses of `TestCoroutineScope`, explicit or implicit.
372
373Replacing `runTestWithLegacyScope` and `runBlockingTest` with `runTest` and `runBlockingTestOnTestScope` should be
374straightforward if there is no more code left that requires passing exactly `TestCoroutineScope` to it.
375Some tests may fail because `TestCoroutineScope.cleanupTestCoroutines` and the cleanup procedure in `runTest`
376handle cancelled tasks differently: if there are *cancelled* jobs pending at the moment of
377`TestCoroutineScope.cleanupTestCoroutines`, they are ignored, whereas `runTest` will report them.
378
379Of all the methods supported by `TestCoroutineScope`, only `cleanupTestCoroutines` is not provided on `TestScope`,
380and its usages should have been removed during the previous step.
381
382## Replace `runBlocking` with `runTest`
383
384Now that `runTest` works properly with asynchronous completions, `runBlocking` is only occasionally useful.
385As is, most uses of `runBlocking` in tests come from the need to interact with dispatchers that execute on other
386threads, like `Dispatchers.IO` or `Dispatchers.Default`.
387
388## Replace `TestCoroutineDispatcher` with `UnconfinedTestDispatcher` and `StandardTestDispatcher`
389
390`TestCoroutineDispatcher` is a dispatcher with two modes:
391* ("unpaused") Almost (but not quite) unconfined, with the ability to eagerly enter `launch` and `async` blocks.
392* ("paused") Behaving like a `StandardTestDispatcher`.
393
394In one of the earlier steps, we replaced `pauseDispatcher` with `StandardTestDispatcher` usage, and replaced the
395implicit `TestCoroutineScope` dispatcher in `runBlockingTest` with `UnconfinedTestDispatcher` during migration to
396`runTest`.
397
398Now, the rest of the usages should be replaced with whichever dispatcher is most appropriate.
399
400## Simplify code by removing unneeded entities
401
402Likely, now some code has the form
403
404```kotlin
405val dispatcher = StandardTestDispatcher()
406val scope = TestScope(dispatcher)
407
408@BeforeTest
409fun setUp() {
410    Dispatchers.setMain(dispatcher)
411}
412
413@AfterTest
414fun tearDown() {
415    Dispatchers.resetMain()
416}
417
418@Test
419fun testFoo() = scope.runTest {
420    // ...
421}
422```
423
424The point of this pattern is to ensure that the test runs with the same `TestCoroutineScheduler` as the one used for
425`Dispatchers.Main`.
426
427However, now this can be simplified to just
428
429```kotlin
430@BeforeTest
431fun setUp() {
432  Dispatchers.setMain(StandardTestDispatcher())
433}
434
435@AfterTest
436fun tearDown() {
437  Dispatchers.resetMain()
438}
439
440@Test
441fun testFoo() = runTest {
442  // ...
443}
444```
445
446The reason this works is that all entities that depend on `TestCoroutineScheduler` will attempt to acquire one from
447the current `Dispatchers.Main`.
448