| # Migration to the new kotlinx-coroutines-test API |
| |
| In version 1.6.0, the API of the test module changed significantly. |
| This is a guide for gradually adapting the existing test code to the new API. |
| This guide is written step-by-step; the idea is to separate the migration into several sets of small changes. |
| |
| ## Remove custom `UncaughtExceptionCaptor`, `DelayController`, and `TestCoroutineScope` implementations |
| |
| We couldn't find any code that defined new implementations of these interfaces, so they are deprecated. It's likely that |
| you don't need to do anything for this section. |
| |
| ### `UncaughtExceptionCaptor` |
| |
| If the code base has an `UncaughtExceptionCaptor`, its special behavior as opposed to just `CoroutineExceptionHandler` |
| was that, at the end of `runBlockingTest` or `cleanupTestCoroutines` (or both), its `cleanupTestCoroutines` procedure |
| was called. |
| |
| We currently don't provide a replacement for this. |
| However, `runTest` follows structured concurrency better than `runBlockingTest` did, so exceptions from child coroutines |
| are propagated structurally, which makes uncaught exception handlers less useful. |
| |
| If you have a use case for this, please tell us about it at the issue tracker. |
| Meanwhile, it should be possible to use a custom exception captor, which should only implement |
| `CoroutineExceptionHandler` now, like this: |
| |
| ```kotlin |
| @Test |
| fun testFoo() = runTest { |
| val customCaptor = MyUncaughtExceptionCaptor() |
| launch(customCaptor) { |
| // ... |
| } |
| advanceUntilIdle() |
| customCaptor.cleanupTestCoroutines() |
| } |
| ``` |
| |
| ### `DelayController` |
| |
| We don't provide a way to define custom dispatching strategies that support virtual time. |
| That said, we significantly enhanced this mechanism: |
| * Using multiple test dispatchers simultaneously is supported. |
| For the dispatchers to have a shared knowledge of the virtual time, either the same `TestCoroutineScheduler` should be |
| passed to each of them, or all of them should be constructed after `Dispatchers.setMain` is called with some test |
| dispatcher. |
| * Both a simple `StandardTestDispatcher` that is always paused, and unconfined `UnconfinedTestDispatcher` are provided. |
| |
| If you have a use case for `DelayController` that's not covered by what we provide, please tell us about it in the issue |
| tracker. |
| |
| ### `TestCoroutineScope` |
| |
| This scope couldn't be meaningfully used in tandem with `runBlockingTest`: according to the definition of |
| `TestCoroutineScope.runBlockingTest`, only the scope's `coroutineContext` is used. |
| So, there could be two reasons for defining a custom implementation: |
| |
| * Avoiding the restrictions on placed `coroutineContext` in the `TestCoroutineScope` constructor function. |
| These restrictions consisted of requirements for `CoroutineExceptionHandler` being an `UncaughtExceptionCaptor`, and |
| `ContinuationInterceptor` being a `DelayController`, so it is also possible to fulfill these restrictions by defining |
| conforming instances. In this case, follow the instructions about replacing them. |
| * Using without `runBlockingTest`. In this case, you don't even need to implement `TestCoroutineScope`: nothing else |
| accepts a `TestCoroutineScope` specifically as an argument. |
| |
| ## Remove usages of `TestCoroutineExceptionHandler` and `TestCoroutineScope.uncaughtExceptions` |
| |
| It is already illegal to use a `TestCoroutineScope` without performing `cleanupTestCoroutines`, so the valid uses of |
| `TestCoroutineExceptionHandler` include: |
| |
| * Accessing `uncaughtExceptions` in the middle of the test to make sure that there weren't any uncaught exceptions |
| *yet*. |
| If there are any, they will be thrown by the cleanup procedure anyway. |
| We don't support this use case, given how comparatively rare it is, but it can be handled in the same way as the |
| following one. |
| * Accessing `uncaughtExceptions` when the uncaught exceptions are actually expected. |
| In this case, `cleanupTestCoroutines` will fail with an exception that is being caught later. |
| It would be better in this case to use a custom `CoroutineExceptionHandler` so that actual problems that could be |
| found by the cleanup procedure are not superseded by the exceptions that are expected. |
| An example is shown below. |
| |
| ```kotlin |
| val exceptions = mutableListOf<Throwable>() |
| val customCaptor = CoroutineExceptionHandler { ctx, throwable -> |
| exceptions.add(throwable) // add proper synchronization if the test is multithreaded |
| } |
| |
| @Test |
| fun testFoo() = runTest { |
| launch(customCaptor) { |
| // ... |
| } |
| advanceUntilIdle() |
| // check the list of the caught exceptions |
| } |
| ``` |
| |
| ## Auto-replace `TestCoroutineScope` constructor function with `createTestCoroutineScope` |
| |
| This should not break anything, as `TestCoroutineScope` is now defined in terms of `createTestCoroutineScope`. |
| If it does break something, it means that you already supplied a `TestCoroutineScheduler` to some scope; in this case, |
| also pass this scheduler as the argument to the dispatcher. |
| |
| ## Replace usages of `pauseDispatcher` and `resumeDispatcher` with a `StandardTestDispatcher` |
| |
| * In places where `pauseDispatcher` in its block form is called, replace it with a call to |
| `withContext(StandardTestDispatcher(testScheduler))` |
| (`testScheduler` is available as a field of `TestCoroutineScope`, |
| or `scheduler` is available as a field of `TestCoroutineDispatcher`), |
| followed by `advanceUntilIdle()`. |
| This is not an automatic replacement, as there can be tricky situations where the test dispatcher is already paused |
| when `pauseDispatcher { X }` is called. In such cases, simply replace `pauseDispatcher { X }` with `X`. |
| * Often, `pauseDispatcher()` in a non-block form is used at the start of the test. |
| Then, attempt to remove `TestCoroutineDispatcher` from the arguments to `createTestCoroutineScope`, |
| if a standalone `TestCoroutineScope` or the `scope.runBlockingTest` form is used, |
| or pass a `StandardTestDispatcher` as an argument to `runBlockingTest`. |
| This will lead to the test using a `StandardTestDispatcher`, which does not allow pausing and resuming, |
| instead of the deprecated `TestCoroutineDispatcher`. |
| * Sometimes, `pauseDispatcher()` and `resumeDispatcher()` are employed used throughout the test. |
| In this case, attempt to wrap everything until the next `resumeDispatcher()` in |
| a `withContext(StandardTestDispatcher(testScheduler))` block, or try using some other combinations of |
| `StandardTestDispatcher` (where dispatches are needed) and `UnconfinedTestDispatcher` (where it isn't important where |
| execution happens). |
| |
| ## Replace `advanceTimeBy(n)` with `advanceTimeBy(n); runCurrent()` |
| |
| For `TestCoroutineScope` and `DelayController`, the `advanceTimeBy` method is deprecated. |
| It is not deprecated for `TestCoroutineScheduler` and `TestScope`, but has a different meaning: it does not run the |
| tasks scheduled *at* `currentTime + n`. |
| |
| There is an automatic replacement for this deprecation, which produces correct but inelegant code. |
| |
| Alternatively, you can wait until replacing `TestCoroutineScope` with `TestScope`: it's possible that you will not |
| encounter this edge case. |
| |
| ## Replace `runBlockingTest` with `runTest(UnconfinedTestDispatcher())` |
| |
| This is a major change, affecting many things, and can be done in parallel with replacing `TestCoroutineScope` with |
| `TestScope`. |
| |
| Significant differences of `runTest` from `runBlockingTest` are each given a section below. |
| |
| ### It works properly with other dispatchers and asynchronous completions. |
| |
| No action on your part is required, other than replacing `runBlocking` with `runTest` as well. |
| |
| ### It uses `StandardTestDispatcher` by default, not `TestCoroutineDispatcher`. |
| |
| By now, calls to `pauseDispatcher` and `resumeDispatcher` should be purged from the code base, so only the unpaused |
| variant of `TestCoroutineDispatcher` should be used. |
| This version of the dispatcher has the property of eagerly entering `launch` and `async` blocks: |
| code until the first suspension is executed without dispatching. |
| |
| There are two common ways in which this property is useful. |
| |
| #### `TestCoroutineDispatcher` for the top-level coroutine |
| |
| Some tests that rely on `launch` and `async` blocks being entered immediately have a form similar to this: |
| ```kotlin |
| runTest(TestCoroutineDispatcher()) { |
| launch { |
| updateSomething() |
| } |
| checkThatSomethingWasUpdated() |
| launch { |
| updateSomethingElse() |
| } |
| checkThatSomethingElseWasUpdated() |
| } |
| ``` |
| |
| If the `TestCoroutineDispatcher()` is simply removed, `StandardTestDispatcher()` will be used, which will cause |
| the test to fail. |
| |
| In these cases, `UnconfinedTestDispatcher()` should be used. |
| We ensured that, when run with an `UnconfinedTestDispatcher`, `runTest` also eagerly enters `launch` and `async` |
| blocks. |
| |
| Note though that *this only works at the top level*: if a child coroutine also called `launch` or `async`, we don't provide |
| any guarantees about their dispatching order. |
| |
| #### `TestCoroutineDispatcher` for testing intermediate emissions |
| |
| Some code tests `StateFlow` or channels in a manner similar to this: |
| |
| ```kotlin |
| @Test |
| fun testAllEmissions() = runTest(TestCoroutineDispatcher()) { |
| val values = mutableListOf<Int>() |
| val stateFlow = MutableStateFlow(0) |
| val job = launch { |
| stateFlow.collect { |
| values.add(it) |
| } |
| } |
| stateFlow.value = 1 |
| stateFlow.value = 2 |
| stateFlow.value = 3 |
| job.cancel() |
| // each assignment will immediately resume the collecting child coroutine, |
| // so no values will be skipped. |
| assertEquals(listOf(0, 1, 2, 3), values) |
| } |
| ``` |
| |
| Such code will fail when `TestCoroutineDispatcher()` is not used: not every emission will be listed. |
| In this particular case, none will be listed at all. |
| |
| The reason for this is that setting `stateFlow.value` (as is sending to a channel, as are some other things) wakes up |
| the coroutine waiting for the new value, but *typically* does not immediately run the collecting code, instead simply |
| dispatching it. |
| The exceptions are the coroutines running in dispatchers that don't (always) go through a dispatch, |
| `Dispatchers.Unconfined`, `Dispatchers.Main.immediate`, `UnconfinedTestDispatcher`, or `TestCoroutineDispatcher` in |
| the unpaused state. |
| |
| Therefore, a solution is to launch the collection in an unconfined dispatcher: |
| |
| ```kotlin |
| @Test |
| fun testAllEmissions() = runTest { |
| val values = mutableListOf<Int>() |
| val stateFlow = MutableStateFlow(0) |
| val job = launch(UnconfinedTestDispatcher(testScheduler)) { // <------ |
| stateFlow.collect { |
| values.add(it) |
| } |
| } |
| stateFlow.value = 1 |
| stateFlow.value = 2 |
| stateFlow.value = 3 |
| job.cancel() |
| // each assignment will immediately resume the collecting child coroutine, |
| // so no values will be skipped. |
| assertEquals(listOf(0, 1, 2, 3), values) |
| } |
| ``` |
| |
| Note that `testScheduler` is passed so that the unconfined dispatcher is linked to `runTest`. |
| Also, note that `UnconfinedTestDispatcher` is not passed to `runTest`. |
| This is due to the fact that, *inside* the `UnconfinedTestDispatcher`, there are no execution order guarantees, |
| so it would not be guaranteed that setting `stateFlow.value` would immediately run the collecting code |
| (though in this case, it does). |
| |
| #### Other considerations |
| |
| Using `UnconfinedTestDispatcher` as an argument to `runTest` will probably lead to the test being executed as it |
| did, but it's still possible that the test relies on the specific dispatching order of `TestCoroutineDispatcher`, |
| so it will need to be tweaked. |
| |
| If some code is expected to have run at some point, but it hasn't, use `runCurrent` to force the tasks scheduled |
| at this moment of time to run. |
| For example, the `StateFlow` example above can also be forced to succeed by doing this: |
| |
| ```kotlin |
| @Test |
| fun testAllEmissions() = runTest { |
| val values = mutableListOf<Int>() |
| val stateFlow = MutableStateFlow(0) |
| val job = launch { |
| stateFlow.collect { |
| values.add(it) |
| } |
| } |
| runCurrent() |
| stateFlow.value = 1 |
| runCurrent() |
| stateFlow.value = 2 |
| runCurrent() |
| stateFlow.value = 3 |
| runCurrent() |
| job.cancel() |
| // each assignment will immediately resume the collecting child coroutine, |
| // so no values will be skipped. |
| assertEquals(listOf(0, 1, 2, 3), values) |
| } |
| ``` |
| |
| Be wary though of this approach: using `runCurrent`, `advanceTimeBy`, or `advanceUntilIdle` is, essentially, |
| simulating some particular execution order, which is not guaranteed to happen in production code. |
| For example, using `UnconfinedTestDispatcher` to fix this test reflects how, in production code, one could use |
| `Dispatchers.Unconfined` to observe all emitted values without conflation, but the `runCurrent()` approach only |
| states that the behavior would be observed if a dispatch were to happen at some chosen points. |
| It is, therefore, recommended to structure tests in a way that does not rely on a particular interleaving, unless |
| that is the intention. |
| |
| ### The job hierarchy is completely different. |
| |
| - Structured concurrency is used, with the scope provided as the receiver of `runTest` actually being the scope of the |
| created coroutine. |
| - Not `SupervisorJob` but a normal `Job` is used for the `TestCoroutineScope`. |
| - The job passed as an argument is used as a parent job. |
| |
| Most tests should not be affected by this. In case your test is, try explicitly launching a child coroutine with a |
| `SupervisorJob`; this should make the job hierarchy resemble what it used to be. |
| |
| ```kotlin |
| @Test |
| fun testFoo() = runTest { |
| val deferred = async(SupervisorJob()) { |
| // test code |
| } |
| advanceUntilIdle() |
| deferred.getCompletionExceptionOrNull()?.let { |
| throw it |
| } |
| } |
| ``` |
| |
| ### Only a single call to `runTest` is permitted per test. |
| |
| In order to work on JS, only a single call to `runTest` must happen during one test, and its result must be returned |
| immediately: |
| |
| ```kotlin |
| @Test |
| fun testFoo(): TestResult { |
| // arbitrary code here |
| return runTest { |
| // ... |
| } |
| } |
| ``` |
| |
| When used only on the JVM, `runTest` will work when called repeatedly, but this is not supported. |
| Please only call `runTest` once per test, and if for some reason you can't, please tell us about in on the issue |
| tracker. |
| |
| ### It uses `TestScope`, not `TestCoroutineScope`, by default. |
| |
| There is a `runTestWithLegacyScope` method that allows migrating from `runBlockingTest` to `runTest` before migrating |
| from `TestCoroutineScope` to `TestScope`, if exactly the `TestCoroutineScope` needs to be passed somewhere else and |
| `TestScope` will not suffice. |
| |
| ## Replace `TestCoroutineScope.cleanupTestCoroutines` with `runTest` |
| |
| Likely can be done together with the next step. |
| |
| Remove all calls to `TestCoroutineScope.cleanupTestCoroutines` from the code base. |
| Instead, as the last step of each test, do `return scope.runTest`; if possible, the whole test body should go inside |
| the `runTest` block. |
| |
| The cleanup procedure in `runTest` will not check that the virtual time doesn't advance during cleanup. |
| If a test must check that no other delays are remaining after it has finished, the following form may help: |
| ```kotlin |
| runTest { |
| testBody() |
| val timeAfterTest = currentTime() |
| advanceUntilIdle() // run the remaining tasks |
| assertEquals(timeAfterTest, currentTime()) // will fail if there were tasks scheduled at a later moment |
| } |
| ``` |
| Note that this will report time advancement even if the job scheduled at a later point was cancelled. |
| |
| It may be the case that `cleanupTestCoroutines` must be executed after de-initialization in `@AfterTest`, which happens |
| outside the test itself. |
| In this case, we propose that you write a wrapper of the form: |
| |
| ```kotlin |
| fun runTestAndCleanup(body: TestScope.() -> Unit) = runTest { |
| try { |
| body() |
| } finally { |
| // the usual cleanup procedures that used to happen before `cleanupTestCoroutines` |
| } |
| } |
| ``` |
| |
| ## Replace `runBlockingTest` with `runBlockingTestOnTestScope`, `createTestCoroutineScope` with `TestScope` |
| |
| Also, replace `runTestWithLegacyScope` with just `runTest`. |
| All of this can be done in parallel with replacing `runBlockingTest` with `runTest`. |
| |
| This step should remove all uses of `TestCoroutineScope`, explicit or implicit. |
| |
| Replacing `runTestWithLegacyScope` and `runBlockingTest` with `runTest` and `runBlockingTestOnTestScope` should be |
| straightforward if there is no more code left that requires passing exactly `TestCoroutineScope` to it. |
| Some tests may fail because `TestCoroutineScope.cleanupTestCoroutines` and the cleanup procedure in `runTest` |
| handle cancelled tasks differently: if there are *cancelled* jobs pending at the moment of |
| `TestCoroutineScope.cleanupTestCoroutines`, they are ignored, whereas `runTest` will report them. |
| |
| Of all the methods supported by `TestCoroutineScope`, only `cleanupTestCoroutines` is not provided on `TestScope`, |
| and its usages should have been removed during the previous step. |
| |
| ## Replace `runBlocking` with `runTest` |
| |
| Now that `runTest` works properly with asynchronous completions, `runBlocking` is only occasionally useful. |
| As is, most uses of `runBlocking` in tests come from the need to interact with dispatchers that execute on other |
| threads, like `Dispatchers.IO` or `Dispatchers.Default`. |
| |
| ## Replace `TestCoroutineDispatcher` with `UnconfinedTestDispatcher` and `StandardTestDispatcher` |
| |
| `TestCoroutineDispatcher` is a dispatcher with two modes: |
| * ("unpaused") Almost (but not quite) unconfined, with the ability to eagerly enter `launch` and `async` blocks. |
| * ("paused") Behaving like a `StandardTestDispatcher`. |
| |
| In one of the earlier steps, we replaced `pauseDispatcher` with `StandardTestDispatcher` usage, and replaced the |
| implicit `TestCoroutineScope` dispatcher in `runBlockingTest` with `UnconfinedTestDispatcher` during migration to |
| `runTest`. |
| |
| Now, the rest of the usages should be replaced with whichever dispatcher is most appropriate. |
| |
| ## Simplify code by removing unneeded entities |
| |
| Likely, now some code has the form |
| |
| ```kotlin |
| val dispatcher = StandardTestDispatcher() |
| val scope = TestScope(dispatcher) |
| |
| @BeforeTest |
| fun setUp() { |
| Dispatchers.setMain(dispatcher) |
| } |
| |
| @AfterTest |
| fun tearDown() { |
| Dispatchers.resetMain() |
| } |
| |
| @Test |
| fun testFoo() = scope.runTest { |
| // ... |
| } |
| ``` |
| |
| The point of this pattern is to ensure that the test runs with the same `TestCoroutineScheduler` as the one used for |
| `Dispatchers.Main`. |
| |
| However, now this can be simplified to just |
| |
| ```kotlin |
| @BeforeTest |
| fun setUp() { |
| Dispatchers.setMain(StandardTestDispatcher()) |
| } |
| |
| @AfterTest |
| fun tearDown() { |
| Dispatchers.resetMain() |
| } |
| |
| @Test |
| fun testFoo() = runTest { |
| // ... |
| } |
| ``` |
| |
| The reason this works is that all entities that depend on `TestCoroutineScheduler` will attempt to acquire one from |
| the current `Dispatchers.Main`. |