|
| 1 | +# Migration to the new kotlinx-coroutines-test API |
| 2 | + |
| 3 | +In version 1.6.0, the API of the test module changed significantly. |
| 4 | +This is a guide for gradually adapting the existing test code to the new API. |
| 5 | +This 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 | + |
| 9 | +We couldn't find any code that defined new implementations of these interfaces, so they are deprecated. It's likely that |
| 10 | +you don't need to do anything for this section. |
| 11 | + |
| 12 | +### `UncaughtExceptionCaptor` |
| 13 | + |
| 14 | +If the code base has an `UncaughtExceptionCaptor`, its special behavior as opposed to just `CoroutineExceptionHandler` |
| 15 | +was that, at the end of `runBlockingTest` or `cleanupTestCoroutines` (or both), its `cleanupTestCoroutines` procedure |
| 16 | +was called. |
| 17 | + |
| 18 | +We currently don't provide a replacement for this. |
| 19 | +However, `runTest` follows structured concurrency better than `runBlockingTest` did, so exceptions from child coroutines |
| 20 | +are propagated structurally, which makes uncaught exception handlers less useful. |
| 21 | + |
| 22 | +If you have a use case for this, please tell us about it at the issue tracker. |
| 23 | +Meanwhile, it should be possible to use a custom exception captor, which should only implement |
| 24 | +`CoroutineExceptionHandler` now, like this: |
| 25 | + |
| 26 | +```kotlin |
| 27 | +@Test |
| 28 | +fun testFoo() = runTest { |
| 29 | + val customCaptor = MyUncaughtExceptionCaptor() |
| 30 | + launch(customCaptor) { |
| 31 | + // ... |
| 32 | + } |
| 33 | + advanceUntilIdle() |
| 34 | + customCaptor.cleanupTestCoroutines() |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | +### `DelayController` |
| 39 | + |
| 40 | +We don't provide a way to define custom dispatching strategies that support virtual time. |
| 41 | +That 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 | + |
| 48 | +If you have a use case for `DelayController` that's not covered by what we provide, please tell us about it in the issue |
| 49 | +tracker. |
| 50 | + |
| 51 | +### `TestCoroutineScope` |
| 52 | + |
| 53 | +This 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. |
| 55 | +So, 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 | + |
| 66 | +It 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 |
| 81 | +val exceptions = mutableListOf<Throwable>() |
| 82 | +val customCaptor = CoroutineExceptionHandler { ctx, throwable -> |
| 83 | + exceptions.add(throwable) // add proper synchronization if the test is multithreaded |
| 84 | +} |
| 85 | + |
| 86 | +@Test |
| 87 | +fun 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 | + |
| 98 | +This should not break anything, as `TestCoroutineScope` is now defined in terms of `createTestCoroutineScope`. |
| 99 | +If it does break something, it means that you already supplied a `TestCoroutineScheduler` to some scope; in this case, |
| 100 | +also 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 | + |
| 125 | +For `TestCoroutineScope` and `DelayController`, the `advanceTimeBy` method is deprecated. |
| 126 | +It is not deprecated for `TestCoroutineScheduler` and `TestScope`, but has a different meaning: it does not run the |
| 127 | +tasks scheduled *at* `currentTime + n`. |
| 128 | + |
| 129 | +There is an automatic replacement for this deprecation, which produces correct but inelegant code. |
| 130 | + |
| 131 | +Alternatively, you can wait until replacing `TestCoroutineScope` with `TestScope`: it's possible that you will not |
| 132 | +encounter this edge case. |
| 133 | + |
| 134 | +## Replace `runBlockingTest` with `runTest(UnconfinedTestDispatcher())` |
| 135 | + |
| 136 | +This is a major change, affecting many things, and can be done in parallel with replacing `TestCoroutineScope` with |
| 137 | +`TestScope`. |
| 138 | + |
| 139 | +Significant differences of `runTest` from `runBlockingTest` are each given a section below. |
| 140 | + |
| 141 | +### It works properly with other dispatchers and asynchronous completions. |
| 142 | + |
| 143 | +No action on your part is required, other than replacing `runBlocking` with `runTest` as well. |
| 144 | + |
| 145 | +### It uses `StandardTestDispatcher` by default, not `TestCoroutineDispatcher`. |
| 146 | + |
| 147 | +By now, calls to `pauseDispatcher` and `resumeDispatcher` should be purged from the code base, so only the unpaused |
| 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: |
| 150 | +code until the first suspension is executed without dispatching. |
| 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 |
| 154 | +any guarantees about their dispatching order. |
| 155 | + |
| 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 |
| 160 | +at this moment of time to run. |
| 161 | + |
| 162 | +### The job hierarchy is completely different. |
| 163 | + |
| 164 | +- Structured concurrency is used, with the scope provided as the receiver of `runTest` actually being the scope of the |
| 165 | + created coroutine. |
| 166 | +- Not `SupervisorJob` but a normal `Job` is used for the `TestCoroutineScope`. |
| 167 | +- The job passed as an argument is used as a parent job. |
| 168 | + |
| 169 | +Most tests should not be affected by this. In case your test is, try explicitly launching a child coroutine with a |
| 170 | +`SupervisorJob`; this should make the job hierarchy resemble what it used to be. |
| 171 | + |
| 172 | +```kotlin |
| 173 | +@Test |
| 174 | +fun testFoo() = runTest { |
| 175 | + val deferred = async(SupervisorJob()) { |
| 176 | + // test code |
| 177 | + } |
| 178 | + advanceUntilIdle() |
| 179 | + deferred.getCompletionExceptionOrNull()?.let { |
| 180 | + throw it |
| 181 | + } |
| 182 | +} |
| 183 | +``` |
| 184 | + |
| 185 | +### Only a single call to `runTest` is permitted per test. |
| 186 | + |
| 187 | +In order to work on JS, only a single call to `runTest` must happen during one test, and its result must be returned |
| 188 | +immediately: |
| 189 | + |
| 190 | +```kotlin |
| 191 | +@Test |
| 192 | +fun testFoo(): TestResult { |
| 193 | + // arbitrary code here |
| 194 | + return runTest { |
| 195 | + // ... |
| 196 | + } |
| 197 | +} |
| 198 | +``` |
| 199 | + |
| 200 | +When used only on the JVM, `runTest` will work when called repeatedly, but this is not supported. |
| 201 | +Please only call `runTest` once per test, and if for some reason you can't, please tell us about in on the issue |
| 202 | +tracker. |
| 203 | + |
| 204 | +### It uses `TestScope`, not `TestCoroutineScope`, by default. |
| 205 | + |
| 206 | +There is a `runTestWithLegacyScope` method that allows migrating from `runBlockingTest` to `runTest` before migrating |
| 207 | +from `TestCoroutineScope` to `TestScope`, if exactly the `TestCoroutineScope` needs to be passed somewhere else and |
| 208 | +`TestScope` will not suffice. |
| 209 | + |
| 210 | +## Replace `TestCoroutineScope.cleanupTestCoroutines` with `runTest` |
| 211 | + |
| 212 | +Likely can be done together with the next step. |
| 213 | + |
| 214 | +Remove all calls to `TestCoroutineScope.cleanupTestCoroutines` from the code base. |
| 215 | +Instead, as the last step of each test, do `return scope.runTest`; if possible, the whole test body should go inside |
| 216 | +the `runTest` block. |
| 217 | + |
| 218 | +The cleanup procedure in `runTest` will not check that the virtual time doesn't advance during cleanup. |
| 219 | +If a test must check that no other delays are remaining after it has finished, the following form may help: |
| 220 | +```kotlin |
| 221 | +runTest { |
| 222 | + testBody() |
| 223 | + val timeAfterTest = currentTime() |
| 224 | + advanceUntilIdle() // run the remaining tasks |
| 225 | + assertEquals(timeAfterTest, currentTime()) // will fail if there were tasks scheduled at a later moment |
| 226 | +} |
| 227 | +``` |
| 228 | +Note that this will report time advancement even if the job scheduled at a later point was cancelled. |
| 229 | + |
| 230 | +It may be the case that `cleanupTestCoroutines` must be executed after de-initialization in `@AfterTest`, which happens |
| 231 | +outside the test itself. |
| 232 | +In this case, we propose that you write a wrapper of the form: |
| 233 | + |
| 234 | +```kotlin |
| 235 | +fun runTestAndCleanup(body: TestScope.() -> Unit) = runTest { |
| 236 | + try { |
| 237 | + body() |
| 238 | + } finally { |
| 239 | + // the usual cleanup procedures that used to happen before `cleanupTestCoroutines` |
| 240 | + } |
| 241 | +} |
| 242 | +``` |
| 243 | + |
| 244 | +## Replace `runBlockingTest` with `runBlockingTestOnTestScope`, `createTestCoroutineScope` with `TestScope` |
| 245 | + |
| 246 | +Also, replace `runTestWithLegacyScope` with just `runTest`. |
| 247 | +All of this can be done in parallel with replacing `runBlockingTest` with `runTest`. |
| 248 | + |
| 249 | +This step should remove all uses of `TestCoroutineScope`, explicit or implicit. |
| 250 | + |
| 251 | +Replacing `runTestWithLegacyScope` and `runBlockingTest` with `runTest` and `runBlockingTestOnTestScope` should be |
| 252 | +straightforward if there is no more code left that requires passing exactly `TestCoroutineScope` to it. |
| 253 | +Some tests may fail because `TestCoroutineScope.cleanupTestCoroutines` and the cleanup procedure in `runTest` |
| 254 | +handle cancelled tasks differently: if there are *cancelled* jobs pending at the moment of |
| 255 | +`TestCoroutineScope.cleanupTestCoroutines`, they are ignored, whereas `runTest` will report them. |
| 256 | + |
| 257 | +Of all the methods supported by `TestCoroutineScope`, only `cleanupTestCoroutines` is not provided on `TestScope`, |
| 258 | +and its usages should have been removed during the previous step. |
| 259 | + |
| 260 | +## Replace `runBlocking` with `runTest` |
| 261 | + |
| 262 | +Now that `runTest` works properly with asynchronous completions, `runBlocking` is only occasionally useful. |
| 263 | +As is, most uses of `runBlocking` in tests come from the need to interact with dispatchers that execute on other |
| 264 | +threads, like `Dispatchers.IO` or `Dispatchers.Default`. |
| 265 | + |
| 266 | +## Replace `TestCoroutineDispatcher` with `UnconfinedTestDispatcher` and `StandardTestDispatcher` |
| 267 | + |
| 268 | +`TestCoroutineDispatcher` is a dispatcher with two modes: |
| 269 | +* ("unpaused") Almost (but not quite) unconfined, with the ability to eagerly enter `launch` and `async` blocks. |
| 270 | +* ("paused") Behaving like a `StandardTestDispatcher`. |
| 271 | + |
| 272 | +In one of the earlier steps, we replaced `pauseDispatcher` with `StandardTestDispatcher` usage, and replaced the |
| 273 | +implicit `TestCoroutineScope` dispatcher in `runBlockingTest` with `UnconfinedTestDispatcher` during migration to |
| 274 | +`runTest`. |
| 275 | + |
| 276 | +Now, the rest of the usages should be replaced with whichever dispatcher is most appropriate. |
| 277 | + |
| 278 | +## Simplify code by removing unneeded entities |
| 279 | + |
| 280 | +Likely, now some code has the form |
| 281 | + |
| 282 | +```kotlin |
| 283 | +val dispatcher = StandardTestDispatcher() |
| 284 | +val scope = TestScope(dispatcher) |
| 285 | + |
| 286 | +@BeforeTest |
| 287 | +fun setUp() { |
| 288 | + Dispatchers.setMain(dispatcher) |
| 289 | +} |
| 290 | + |
| 291 | +@AfterTest |
| 292 | +fun tearDown() { |
| 293 | + Dispatchers.resetMain() |
| 294 | +} |
| 295 | + |
| 296 | +@Test |
| 297 | +fun testFoo() = scope.runTest { |
| 298 | + // ... |
| 299 | +} |
| 300 | +``` |
| 301 | + |
| 302 | +The point of this pattern is to ensure that the test runs with the same `TestCoroutineScheduler` as the one used for |
| 303 | +`Dispatchers.Main`. |
| 304 | + |
| 305 | +However, now this can be simplified to just |
| 306 | + |
| 307 | +```kotlin |
| 308 | +@BeforeTest |
| 309 | +fun setUp() { |
| 310 | + Dispatchers.setMain(StandardTestDispatcher()) |
| 311 | +} |
| 312 | + |
| 313 | +@AfterTest |
| 314 | +fun tearDown() { |
| 315 | + Dispatchers.resetMain() |
| 316 | +} |
| 317 | + |
| 318 | +@Test |
| 319 | +fun testFoo() = runTest { |
| 320 | + // ... |
| 321 | +} |
| 322 | +``` |
| 323 | + |
| 324 | +The reason this works is that all entities that depend on `TestCoroutineScheduler` will attempt to acquire one from |
| 325 | +the current `Dispatchers.Main`. |
0 commit comments