Skip to content

Commit 90e250d

Browse files
committed
WIP
1 parent 78d1698 commit 90e250d

File tree

2 files changed

+563
-173
lines changed

2 files changed

+563
-173
lines changed

kotlinx-coroutines-test/MIGRATION.md

+324
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
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 in more cases than before, 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+
@Test
82+
fun testFoo() = runTest {
83+
val exceptions = mutableListOf<Throwable>()
84+
val customCaptor = CoroutineExceptionHandler { ctx, throwable ->
85+
exceptions.add(throwable) // add proper synchronization if the test is multithreaded
86+
}
87+
launch(customCaptor) {
88+
// ...
89+
}
90+
advanceUntilIdle()
91+
// check the list of the caught exceptions
92+
}
93+
```
94+
95+
## Auto-replace `TestCoroutineScope` constructor function with `createTestCoroutineScope`
96+
97+
This should not break anything, as `TestCoroutineScope` is now defined in terms of `createTestCoroutineScope`.
98+
If it does break something, it means that you already supplied a `TestCoroutineScheduler` to some scope; in this case,
99+
also pass this scheduler as the argument to the dispatcher.
100+
101+
## Replace usages of `pauseDispatcher` and `resumeDispatcher` with a `StandardTestDispatcher`
102+
103+
* In places where `pauseDispatcher` in its block form is called, replace it with a call to
104+
`withContext(StandardTestDispatcher(testScheduler))`
105+
(`testScheduler` is available as a field of `TestCoroutineScope`,
106+
or `scheduler` is available as a field of `TestCoroutineDispatcher`),
107+
followed by `advanceUntilIdle()`.
108+
This is not an automatic replacement, as there can be tricky situations where the test dispatcher is already paused
109+
when `pauseDispatcher { X }` is called. In such cases, simply replace `pauseDispatcher { X }` with `X`.
110+
* Often, `pauseDispatcher()` in a non-block form is used at the start of the test.
111+
Then, attempt to remove `TestCoroutineDispatcher` from the arguments to `createTestCoroutineScope`,
112+
if a standalone `TestCoroutineScope` or the `scope.runBlockingTest` form is used,
113+
or pass a `StandardTestDispatcher` as an argument to `runBlockingTest`.
114+
This will lead to the test using a `StandardTestDispatcher`, which does not allow pausing and resuming,
115+
instead of the deprecated `TestCoroutineDispatcher`.
116+
* Sometimes, `pauseDispatcher()` and `resumeDispatcher()` are employed used throughout the test.
117+
In this case, attempt to wrap everything until the next `resumeDispatcher()` in
118+
a `withContext(StandardTestDispatcher(testScheduler))` block, or try using some other combinations of
119+
`StandardTestDispatcher` (where dispatches are needed) and `UnconfinedTestDispatcher` (where it isn't important where
120+
execution happens).
121+
122+
## Replace `advanceTimeBy(n)` with `advanceTimeBy(n); runCurrent()`
123+
124+
For `TestCoroutineScope` and `DelayController`, the `advanceTimeBy` method is deprecated.
125+
It is not deprecated for `TestCoroutineScheduler` and `TestScope`, but has a different meaning: it does not run the
126+
tasks scheduled *at* `currentTime + n`.
127+
128+
There is an automatic replacement for this deprecation, which produces correct but inelegant code.
129+
130+
Alternatively, you can wait until replacing `TestCoroutineScope` with `TestScope`: it's possible that you will not
131+
encounter this edge case.
132+
133+
## Replace `runBlockingTest` with `runTest(UnconfinedTestDispatcher())`
134+
135+
This is a major change, affecting many things, and can be done in parallel with replacing `TestCoroutineScope` with
136+
`TestScope`.
137+
138+
Significant differences of `runTest` from `runBlockingTest` are each given a section below.
139+
140+
### It works properly with other dispatchers and asynchronous completions.
141+
142+
No action on your part is required, other than replacing `runBlocking` with `runTest` as well.
143+
144+
### It uses `StandardTestDispatcher` by default, not `TestCoroutineDispatcher`.
145+
146+
By now, calls to `pauseDispatcher` and `resumeDispatcher` should be purged from the code base, so only the unpaused
147+
variant of `TestCoroutineDispatcher` should be used.
148+
This version of the dispatcher, which can be observed has the property of eagerly entering `launch` and `async` blocks:
149+
code until the first suspension is executed without dispatching.
150+
151+
We ensured sure that, when run with an `UnconfinedTestDispatcher`, `runTest` also eagerly enters `launch` and `async`
152+
blocks, but *this only works at the top level*: if a child coroutine also called `launch` or `async`, we don't provide
153+
any guarantees about their dispatching order.
154+
155+
So, using `UnconfinedTestDispatcher` as an argument to `runTest` will probably lead to the test being executed as it
156+
did, but in the possible case that the test relies on the specific dispatching order of `TestCoroutineDispatcher`, it
157+
will need to be tweaked.
158+
If the test expects some code to have run at some point, but it hasn't, use `runCurrent` to force the tasks scheduled
159+
at this moment of time to run.
160+
161+
### The job hierarchy is completely different.
162+
163+
- Structured concurrency is used, with the scope provided as the receiver of `runTest` actually being the scope of the
164+
created coroutine.
165+
- Not `SupervisorJob` but a normal `Job` is used for the `TestCoroutineScope`.
166+
- The job passed as an argument is used as a parent job.
167+
168+
Most tests should not be affected by this. In case your test is, try explicitly launching a child coroutine with a
169+
`SupervisorJob`; this should make the job hierarchy resemble what it used to be.
170+
171+
```kotlin
172+
@Test
173+
fun testFoo() = runTest {
174+
val deferred = async(SupervisorJob()) {
175+
// test code
176+
}
177+
advanceUntilIdle()
178+
deferred.getCompletionExceptionOrNull()?.let {
179+
throw it
180+
}
181+
}
182+
```
183+
184+
### Only a single call to `runTest` is permitted per test.
185+
186+
In order to work on JS, only a single call to `runTest` must happen during one test, and its result must be returned
187+
immediately:
188+
189+
```kotlin
190+
@Test
191+
fun testFoo(): TestResult {
192+
// arbitrary code here
193+
return runTest {
194+
// ...
195+
}
196+
}
197+
```
198+
199+
When used only on the JVM, `runTest` will work when called repeatedly, but this is not supported.
200+
Please only call `runTest` once per test, and if for some reason you can't, please tell us about in on the issue
201+
tracker.
202+
203+
### It uses `TestScope`, not `TestCoroutineScope`, by default.
204+
205+
There is a `runTestWithLegacyScope` method that allows migrating from `runBlockingTest` to `runTest` before migrating
206+
from `TestCoroutineScope` to `TestScope`, if exactly the `TestCoroutineScope` needs to be passed somewhere else and
207+
`TestScope` will not suffice.
208+
209+
## Replace `TestCoroutineScope.cleanupTestCoroutines` with `runTest`
210+
211+
Likely can be done together with the next step.
212+
213+
Remove all calls to `TestCoroutineScope.cleanupTestCoroutines` from the code base.
214+
Instead, as the last step of each test, do `return scope.runTest`; if possible, the whole test body should go inside
215+
the `runTest` block.
216+
217+
The cleanup procedure in `runTest` will not check that the virtual time doesn't advance during cleanup.
218+
If a test must check that no other delays are remaining after it has finished, the following form may help:
219+
```kotlin
220+
runTest {
221+
testBody()
222+
val timeAfterTest = currentTime()
223+
advanceUntilIdle() // run the remaining tasks
224+
assertEquals(timeAfterTest, currentTime()) // will fail if there were tasks scheduled at a later moment
225+
}
226+
```
227+
Note that this will report time advancement even if the job scheduled at a later point was cancelled.
228+
229+
It may be the case that `cleanupTestCoroutines` must be executed after de-initialization in `@AfterTest`, which happens
230+
outside the test itself.
231+
In this case, we propose that you write a wrapper of the form:
232+
233+
```kotlin
234+
fun runTestAndCleanup(body: TestScope.() -> Unit) = runTest {
235+
try {
236+
body()
237+
} finally {
238+
// the usual cleanup procedures that used to happen before `cleanupTestCoroutines`
239+
}
240+
}
241+
```
242+
243+
## Replace `runBlockingTest` with `runBlockingTestOnTestScope`, `createTestCoroutineScope` with `TestScope`
244+
245+
Also, replace `runTestWithLegacyScope` with just `runTest`.
246+
All of this can be done in parallel with replacing `runBlockingTest` with `runTest`.
247+
248+
This step should remove all uses of `TestCoroutineScope`, explicit or implicit.
249+
250+
Replacing `runTestWithLegacyScope` and `runBlockingTest` with `runTest` and `runBlockingTestOnTestScope` should be
251+
straightforward if there is no more code left that requires passing exactly `TestCoroutineScope` to it.
252+
Some tests may fail because `TestCoroutineScope.cleanupTestCoroutines` and the cleanup procedure in `runTest`
253+
handle cancelled tasks differently: if there are *cancelled* jobs pending at the moment of
254+
`TestCoroutineScope.cleanupTestCoroutines`, they are ignored, whereas `runTest` will report them.
255+
256+
Of all the methods supported by `TestCoroutineScope`, only `cleanupTestCoroutines` is not provided on `TestScope`,
257+
and its usages should have been removed during the previous step.
258+
259+
## Replace `runBlocking` with `runTest`
260+
261+
Now that `runTest` works properly with asynchronous completions, `runBlocking` is only occasionally useful.
262+
As is, most uses of `runBlocking` in tests come from the need to interact with dispatchers that execute on other
263+
threads, like `Dispatchers.IO` or `Dispatchers.Default`.
264+
265+
## Replace `TestCoroutineDispatcher` with `UnconfinedTestDispatcher` and `StandardTestDispatcher`
266+
267+
`TestCoroutineDispatcher` is a dispatcher with two modes:
268+
* ("unpaused") Almost (but not quite) unconfined, with the ability to eagerly enter `launch` and `async` blocks.
269+
* ("paused") Behaving like a `StandardTestDispatcher`.
270+
271+
In one of the earlier steps, we replaced `pauseDispatcher` with `StandardTestDispatcher` usage, and replaced the
272+
implicit `TestCoroutineScope` dispatcher in `runBlockingTest` with `UnconfinedTestDispatcher` during migration to
273+
`runTest`.
274+
275+
Now, the rest of the usages should be replaced with whichever dispatcher is most appropriate.
276+
277+
## Simplify code by removing unneeded entities
278+
279+
Likely, now some code has the form
280+
281+
```kotlin
282+
val dispatcher = StandardTestDispatcher()
283+
val scope = TestScope(dispatcher)
284+
285+
@BeforeTest
286+
fun setUp() {
287+
Dispatchers.setMain(dispatcher)
288+
}
289+
290+
@AfterTest
291+
fun tearDown() {
292+
Dispatchers.resetMain()
293+
}
294+
295+
@Test
296+
fun testFoo() = scope.runTest {
297+
// ...
298+
}
299+
```
300+
301+
The point of this pattern is to ensure that the test runs with the same `TestCoroutineScheduler` as the one used for
302+
`Dispatchers.Main`.
303+
304+
However, now this can be simplified to just
305+
306+
```kotlin
307+
@BeforeTest
308+
fun setUp() {
309+
Dispatchers.setMain(StandardTestDispatcher())
310+
}
311+
312+
@AfterTest
313+
fun tearDown() {
314+
Dispatchers.resetMain()
315+
}
316+
317+
@Test
318+
fun testFoo() = runTest {
319+
// ...
320+
}
321+
```
322+
323+
The reason this works is that all entities that depend on `TestCoroutineScheduler` will attempt to acquire one from
324+
the current `Dispatchers.Main`.

0 commit comments

Comments
 (0)