diff --git a/kotlinx-coroutines-test/MIGRATION.md b/kotlinx-coroutines-test/MIGRATION.md new file mode 100644 index 0000000000..5124864745 --- /dev/null +++ b/kotlinx-coroutines-test/MIGRATION.md @@ -0,0 +1,325 @@ +# 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() +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, which can be observed has the property of eagerly entering `launch` and `async` blocks: +code until the first suspension is executed without dispatching. + +We ensured sure that, when run with an `UnconfinedTestDispatcher`, `runTest` also eagerly enters `launch` and `async` +blocks, but *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. + +So, using `UnconfinedTestDispatcher` as an argument to `runTest` will probably lead to the test being executed as it +did, but in the possible case that the test relies on the specific dispatching order of `TestCoroutineDispatcher`, it +will need to be tweaked. +If the test expects some code to have run at some point, but it hasn't, use `runCurrent` to force the tasks scheduled +at this moment of time to run. + +### 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`. \ No newline at end of file diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index 5130da1167..54450b1e82 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -2,7 +2,24 @@ Test utilities for `kotlinx.coroutines`. -This package provides testing utilities for effectively testing coroutines. +## Overview + +This package provides utilities for efficiently testing coroutines. + +| Name | Description | +| ---- | ----------- | +| [runTest] | Runs the test code, automatically skipping delays and handling uncaught exceptions. | +| [TestCoroutineScheduler] | The shared source of virtual time, used for controlling execution order and skipping delays. | +| [TestScope] | A [CoroutineScope] that integrates with [runTest], providing access to [TestCoroutineScheduler]. | +| [TestDispatcher] | A [CoroutineDispatcher] that whose delays are controlled by a [TestCoroutineScheduler]. | +| [Dispatchers.setMain] | Mocks the main dispatcher using the provided one. If mocked with a [TestDispatcher], its [TestCoroutineScheduler] is used everywhere by default. | + +Provided [TestDispatcher] implementations: + +| Name | Description | +| ---- | ----------- | +| [StandardTestDispatcher] | A simple dispatcher with no special behavior other than being linked to a [TestCoroutineScheduler]. | +| [UnconfinedTestDispatcher] | A dispatcher that behaves like [Dispatchers.Unconfined]. | ## Using in your project @@ -13,24 +30,26 @@ dependencies { } ``` -**Do not** depend on this project in your main sources, all utilities are intended and designed to be used only from tests. +**Do not** depend on this project in your main sources, all utilities here are intended and designed to be used only from tests. ## Dispatchers.Main Delegation -`Dispatchers.setMain` will override the `Main` dispatcher in test situations. This is helpful when you want to execute a -test in situations where the platform `Main` dispatcher is not available, or you wish to replace `Dispatchers.Main` with a -testing dispatcher. +`Dispatchers.setMain` will override the `Main` dispatcher in test scenarios. +This is helpful when one wants to execute a test in situations where the platform `Main` dispatcher is not available, +or to replace `Dispatchers.Main` with a testing dispatcher. -Once you have this dependency in the runtime, -[`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) mechanism will overwrite -[Dispatchers.Main] with a testable implementation. +On the JVM, +the [`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) mechanism is responsible +for overwriting [Dispatchers.Main] with a testable implementation, which by default will delegate its calls to the real +`Main` dispatcher, if any. -You can override the `Main` implementation using [setMain][setMain] method with any [CoroutineDispatcher] implementation, e.g.: +The `Main` implementation can be overridden using [Dispatchers.setMain][setMain] method with any [CoroutineDispatcher] +implementation, e.g.: ```kotlin class SomeTest { - + private val mainThreadSurrogate = newSingleThreadContext("UI thread") @Before @@ -40,10 +59,10 @@ class SomeTest { @After fun tearDown() { - Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher + Dispatchers.resetMain() // reset the main dispatcher to the original Main dispatcher mainThreadSurrogate.close() } - + @Test fun testSomeUI() = runBlocking { launch(Dispatchers.Main) { // Will be launched in the mainThreadSurrogate dispatcher @@ -52,372 +71,289 @@ class SomeTest { } } ``` -Calling `setMain` or `resetMain` immediately changes the `Main` dispatcher globally. The testable version of -`Dispatchers.Main` installed by the `ServiceLoader` will delegate to the dispatcher provided by `setMain`. -## runBlockingTest +Calling `setMain` or `resetMain` immediately changes the `Main` dispatcher globally. -To test regular suspend functions or coroutines started with `launch` or `async` use the [runBlockingTest] coroutine -builder that provides extra test control to coroutines. +If `Main` is overridden with a [TestDispatcher], then its [TestCoroutineScheduler] is used when new [TestDispatcher] or +[TestScope] instances are created without [TestCoroutineScheduler] being passed as an argument. -1. Auto-advancing of time for regular suspend functions -2. Explicit time control for testing multiple coroutines -3. Eager execution of `launch` or `async` code blocks -4. Pause, manually advance, and restart the execution of coroutines in a test -5. Report uncaught exceptions as test failures +## runTest -### Testing regular suspend functions +[runTest] is the way to test code that involves coroutines. `suspend` functions can be called inside it. -To test regular suspend functions, which may have a delay, you can use the [runBlockingTest] builder to start a testing -coroutine. Any calls to `delay` will automatically advance virtual time by the amount delayed. +**IMPORTANT: in order to work with on Kotlin/JS, the result of `runTest` must be immediately `return`-ed from each test.** +The typical invocation of [runTest] thus looks like this: ```kotlin @Test -fun testFoo() = runBlockingTest { // a coroutine with an extra test control - val actual = foo() - // ... +fun testFoo() = runTest { + // code under test } +``` -suspend fun foo() { - delay(1_000) // auto-advances virtual time by 1_000ms due to runBlockingTest - // ... +In more advanced scenarios, it's possible instead to use the following form: +```kotlin +@Test +fun testFoo(): TestResult { + // initialize some test state + return runTest { + // code under test + } } ``` -`runBlockingTest` returns `Unit` so it may be used in a single expression with common testing libraries. +[runTest] is similar to running the code with `runBlocking` on Kotlin/JVM and Kotlin/Native, or launching a new promise +on Kotlin/JS. The main differences are the following: -### Testing `launch` or `async` +* **The calls to `delay` are automatically skipped**, preserving the relative execution order of the tasks. This way, + it's possible to make tests finish more-or-less immediately. +* **Controlling the virtual time**: in case just skipping delays is not sufficient, it's possible to more carefully + guide the execution, advancing the virtual time by a duration, draining the queue of the awaiting tasks, or running + the tasks scheduled at the present moment. +* **Handling uncaught exceptions** spawned in the child coroutines by throwing them at the end of the test. +* **Waiting for asynchronous callbacks**. + Sometimes, especially when working with third-party code, it's impossible to mock all the dispatchers in use. + [runTest] will handle the situations where some code runs in dispatchers not integrated with the test module. -Inside of [runBlockingTest], both [launch] and [async] will start a new coroutine that may run concurrently with the -test case. +## Delay-skipping -To make common testing situations easier, by default the body of the coroutine is executed *eagerly* until -the first call to [delay] or [yield]. +To test regular suspend functions, which may have a delay, just run them inside the [runTest] block. ```kotlin @Test -fun testFooWithLaunch() = runBlockingTest { - foo() - // the coroutine launched by foo() is completed before foo() returns +fun testFoo() = runTest { // a coroutine with an extra test control + val actual = foo() // ... } -fun CoroutineScope.foo() { - // This coroutines `Job` is not shared with the test code - launch { - bar() // executes eagerly when foo() is called due to runBlockingTest - println(1) // executes eagerly when foo() is called due to runBlockingTest - } +suspend fun foo() { + delay(1_000) // when run in `runTest`, will finish immediately instead of delaying + // ... } - -suspend fun bar() {} ``` -`runBlockingTest` will auto-progress virtual time until all coroutines are completed before returning. If any coroutines -are not able to complete, an `AssertionError` will be thrown. - -*Note:* The default eager behavior of [runBlockingTest] will ignore [CoroutineStart] parameters. - -### Testing `launch` or `async` with `delay` +## `launch` and `async` -If the coroutine created by `launch` or `async` calls `delay` then the [runBlockingTest] will not auto-progress time -right away. This allows tests to observe the interaction of multiple coroutines with different delays. +The coroutine dispatcher used for tests is single-threaded, meaning that the child coroutines of the [runTest] block +will run on the thread that started the test, and will never run in parallel. -To control time in the test you can use the [DelayController] interface. The block passed to -[runBlockingTest] can call any method on the `DelayController` interface. +If several coroutines are waiting to be executed next, the one scheduled after the smallest delay will be chosen. +The virtual time will automatically advance to the point of its resumption. ```kotlin @Test -fun testFooWithLaunchAndDelay() = runBlockingTest { - foo() - // the coroutine launched by foo has not completed here, it is suspended waiting for delay(1_000) - advanceTimeBy(1_000) // progress time, this will cause the delay to resume - // the coroutine launched by foo has completed here - // ... -} - -suspend fun CoroutineScope.foo() { +fun testWithMultipleDelays() = runTest { launch { - println(1) // executes eagerly when foo() is called due to runBlockingTest - delay(1_000) // suspends until time is advanced by at least 1_000 - println(2) // executes after advanceTimeBy(1_000) + delay(1_000) + println("1. $currentTime") // 1000 + delay(200) + println("2. $currentTime") // 1200 + delay(2_000) + println("4. $currentTime") // 3200 + } + val deferred = async { + delay(3_000) + println("3. $currentTime") // 3000 + delay(500) + println("5. $currentTime") // 3500 } + deferred.await() } ``` -*Note:* `runBlockingTest` will always attempt to auto-progress time until all coroutines are completed just before -exiting. This is a convenience to avoid having to call [advanceUntilIdle][DelayController.advanceUntilIdle] -as the last line of many common test cases. -If any coroutines cannot complete by advancing time, an `AssertionError` is thrown. +## Controlling the virtual time -### Testing `withTimeout` using `runBlockingTest` - -Time control can be used to test timeout code. To do so, ensure that the function under test is suspended inside a -`withTimeout` block and advance time until the timeout is triggered. - -Depending on the code, causing the code to suspend may need to use different mocking or fake techniques. For this -example an uncompleted `Deferred` is provided to the function under test via parameter injection. +Inside [runTest], the following operations are supported: +* `currentTime` gets the current virtual time. +* `runCurrent()` runs the tasks that are scheduled at this point of virtual time. +* `advanceUntilIdle()` runs all enqueued tasks until there are no more. +* `advanceTimeBy(timeDelta)` runs the enqueued tasks until the current virtual time advances by `timeDelta`. ```kotlin -@Test(expected = TimeoutCancellationException::class) -fun testFooWithTimeout() = runBlockingTest { - val uncompleted = CompletableDeferred() // this Deferred will never complete - foo(uncompleted) - advanceTimeBy(1_000) // advance time, which will cause the timeout to throw an exception - // ... -} - -fun CoroutineScope.foo(resultDeferred: Deferred) { +@Test +fun testFoo() = runTest { launch { - withTimeout(1_000) { - resultDeferred.await() // await() will suspend forever waiting for uncompleted - // ... - } + println(1) // executes during runCurrent() + delay(1_000) // suspends until time is advanced by at least 1_000 + println(2) // executes during advanceTimeBy(2_000) + delay(500) // suspends until the time is advanced by another 500 ms + println(3) // also executes during advanceTimeBy(2_000) + delay(5_000) // will suspend by another 4_500 ms + println(4) // executes during advanceUntilIdle() } + // the child coroutine has not run yet + runCurrent() + // the child coroutine has called println(1), and is suspended on delay(1_000) + advanceTimeBy(2_000) // progress time, this will cause two calls to `delay` to resume + // the child coroutine has called println(2) and println(3) and suspends for another 4_500 virtual milliseconds + advanceUntilIdle() // will run the child coroutine to completion + assertEquals(6500, currentTime) // the child coroutine finished at virtual time of 6_500 milliseconds } ``` -*Note:* Testing timeouts is simpler with a second coroutine that can be suspended (as in this example). If the -call to `withTimeout` is in a regular suspend function, consider calling `launch` or `async` inside your test body to -create a second coroutine. - -### Using `pauseDispatcher` for explicit execution of `runBlockingTest` +## Using multiple test dispatchers -The eager execution of `launch` and `async` bodies makes many tests easier, but some tests need more fine grained -control of coroutine execution. +The virtual time is controlled by an entity called the [TestCoroutineScheduler], which behaves as the shared source of +virtual time. -To disable eager execution, you can call [pauseDispatcher][DelayController.pauseDispatcher] -to pause the [TestCoroutineDispatcher] that [runBlockingTest] uses. +Several dispatchers can be created that use the same [TestCoroutineScheduler], in which case they will share their +knowledge of the virtual time. -When the dispatcher is paused, all coroutines will be added to a queue instead running. In addition, time will never -auto-progress due to `delay` on a paused dispatcher. +To access the scheduler used for this test, use the [TestScope.testScheduler] property. ```kotlin @Test -fun testFooWithPauseDispatcher() = runBlockingTest { - pauseDispatcher { - foo() - // the coroutine started by foo has not run yet - runCurrent() // the coroutine started by foo advances to delay(1_000) - // the coroutine started by foo has called println(1), and is suspended on delay(1_000) - advanceTimeBy(1_000) // progress time, this will cause the delay to resume - // the coroutine started by foo has called println(2) and has completed here - } - // ... -} - -fun CoroutineScope.foo() { - launch { - println(1) // executes after runCurrent() is called - delay(1_000) // suspends until time is advanced by at least 1_000 - println(2) // executes after advanceTimeBy(1_000) +fun testWithMultipleDispatchers() = runTest { + val scheduler = testScheduler // the scheduler used for this test + val dispatcher1 = StandardTestDispatcher(scheduler, name = "IO dispatcher") + val dispatcher2 = StandardTestDispatcher(scheduler, name = "Background dispatcher") + launch(dispatcher1) { + delay(1_000) + println("1. $currentTime") // 1000 + delay(200) + println("2. $currentTime") // 1200 + delay(2_000) + println("4. $currentTime") // 3200 + } + val deferred = async(dispatcher2) { + delay(3_000) + println("3. $currentTime") // 3000 + delay(500) + println("5. $currentTime") // 3500 + } + deferred.await() } -} ``` -Using `pauseDispatcher` gives tests explicit control over the progress of time as well as the ability to enqueue all -coroutines. As a best practice consider adding two tests, one paused and one eager, to test coroutines that have -non-trivial external dependencies and side effects in their launch body. - -*Important:* When passed a lambda block, `pauseDispatcher` will resume eager execution immediately after the block. -This will cause time to auto-progress if there are any outstanding `delay` calls that were not resolved before the -`pauseDispatcher` block returned. In advanced situations tests can call [pauseDispatcher][DelayController.pauseDispatcher] -without a lambda block and then explicitly resume the dispatcher with [resumeDispatcher][DelayController.resumeDispatcher]. +**Note: if [Dispatchers.Main] is replaced by a [TestDispatcher], [runTest] will automatically use its scheduler. +This is done so that there is no need to go through the ceremony of passing the correct scheduler to [runTest].** -## Integrating tests with structured concurrency +## Accessing the test coroutine scope -Code that uses structured concurrency needs a [CoroutineScope] in order to launch a coroutine. In order to integrate -[runBlockingTest] with code that uses common structured concurrency patterns tests can provide one (or both) of these -classes to application code. +Structured concurrency ties coroutines to scopes in which they are launched. +[TestScope] is a special coroutine scope designed for testing coroutines, and a new one is automatically created +for [runTest] and used as the receiver for the test body. - | Name | Description | - | ---- | ----------- | - | [TestCoroutineScope] | A [CoroutineScope] which provides detailed control over the execution of coroutines for tests and integrates with [runBlockingTest]. | - | [TestCoroutineDispatcher] | A [CoroutineDispatcher] which can be used for tests and integrates with [runBlockingTest]. | - - Both classes are provided to allow for various testing needs. Depending on the code that's being - tested, it may be easier to provide a [TestCoroutineDispatcher]. For example [Dispatchers.setMain][setMain] will accept - a [TestCoroutineDispatcher] but not a [TestCoroutineScope]. - - [TestCoroutineScope] will always use a [TestCoroutineDispatcher] to execute coroutines. It - also uses [TestCoroutineExceptionHandler] to convert uncaught exceptions into test failures. +However, it can be convenient to access a `CoroutineScope` before the test has started, for example, to perform mocking +of some +parts of the system in `@BeforeTest` via dependency injection. +In these cases, it is possible to manually create [TestScope], the scope for the test coroutines, in advance, +before the test begins. -By providing [TestCoroutineScope] a test case is able to control execution of coroutines, as well as ensure that -uncaught exceptions thrown by coroutines are converted into test failures. +[TestScope] on its own does not automatically run the code launched in it. +In addition, it is stateful in order to keep track of executing coroutines and uncaught exceptions. +Therefore, it is important to ensure that [TestScope.runTest] is called eventually. -### Providing `TestCoroutineScope` from `runBlockingTest` +```kotlin +val scope = TestScope() -In simple cases, tests can use the [TestCoroutineScope] created by [runBlockingTest] directly. +@BeforeTest +fun setUp() { + Dispatchers.setMain(StandardTestDispatcher(scope.testScheduler)) + TestSubject.setScope(scope) +} -```kotlin -@Test -fun testFoo() = runBlockingTest { - foo() // runBlockingTest passed in a TestCoroutineScope as this +@AfterTest +fun tearDown() { + Dispatchers.resetMain() + TestSubject.resetScope() } -fun CoroutineScope.foo() { - launch { // CoroutineScope for launch is the TestCoroutineScope provided by runBlockingTest - // ... - } +@Test +fun testSubject() = scope.runTest { + // the receiver here is `testScope` } ``` -This style is preferred when the `CoroutineScope` is passed through an extension function style. - -### Providing an explicit `TestCoroutineScope` - -In many cases, the direct style is not preferred because [CoroutineScope] may need to be provided through another means -such as dependency injection or service locators. +## Eagerly entering `launch` and `async` blocks -Tests can declare a [TestCoroutineScope] explicitly in the class to support these use cases. +Some tests only test functionality and don't particularly care about the precise order in which coroutines are +dispatched. +In these cases, it can be cumbersome to always call [runCurrent] or [yield] to observe the effects of the coroutines +after they are launched. -Since [TestCoroutineScope] is stateful in order to keep track of executing coroutines and uncaught exceptions, it is -important to ensure that [cleanupTestCoroutines][TestCoroutineScope.cleanupTestCoroutines] is called after every test case. +If [runTest] executes with an [UnconfinedTestDispatcher], the child coroutines launched at the top level are entered +*eagerly*, that is, they don't go through a dispatch until the first suspension. ```kotlin -class TestClass { - private val testScope = TestCoroutineScope() - private lateinit var subject: Subject - - @Before - fun setup() { - // provide the scope explicitly, in this example using a constructor parameter - subject = Subject(testScope) - } - - @After - fun cleanUp() { - testScope.cleanupTestCoroutines() - } - - @Test - fun testFoo() = testScope.runBlockingTest { - // TestCoroutineScope.runBlockingTest uses the Dispatcher and exception handler provided by `testScope` - subject.foo() - } -} - -class Subject(val scope: CoroutineScope) { - fun foo() { - scope.launch { - // launch uses the testScope injected in setup - } +@Test +fun testEagerlyEnteringChildCoroutines() = runTest(UnconfinedTestDispatcher()) { + var entered = false + val deferred = CompletableDeferred() + var completed = false + launch { + entered = true + deferred.await() + completed = true } + assertTrue(entered) // `entered = true` already executed. + assertFalse(completed) // however, the child coroutine then suspended, so it is enqueued. + deferred.complete(Unit) // resume the coroutine. + assertTrue(completed) // now the child coroutine is immediately completed. } ``` -*Note:* [TestCoroutineScope], [TestCoroutineDispatcher], and [TestCoroutineExceptionHandler] are interfaces to enable -test libraries to provide library specific integrations. For example, a JUnit4 `@Rule` may call -[Dispatchers.setMain][setMain] then expose [TestCoroutineScope] for use in tests. - -### Providing an explicit `TestCoroutineDispatcher` - -While providing a [TestCoroutineScope] is slightly preferred due to the improved uncaught exception handling, there are -many situations where it is easier to provide a [TestCoroutineDispatcher]. For example [Dispatchers.setMain][setMain] -does not accept a [TestCoroutineScope] and requires a [TestCoroutineDispatcher] to control coroutine execution in -tests. - -The main difference between `TestCoroutineScope` and `TestCoroutineDispatcher` is how uncaught exceptions are handled. -When using `TestCoroutineDispatcher` uncaught exceptions thrown in coroutines will use regular -[coroutine exception handling](https://kotlinlang.org/docs/reference/coroutines/exception-handling.html). -`TestCoroutineScope` will always use `TestCoroutineDispatcher` as it's dispatcher. - -A test can use a `TestCoroutineDispatcher` without declaring an explicit `TestCoroutineScope`. This is preferred -when the class under test allows a test to provide a [CoroutineDispatcher] but does not allow the test to provide a -[CoroutineScope]. - -Since [TestCoroutineDispatcher] is stateful in order to keep track of executing coroutines, it is -important to ensure that [cleanupTestCoroutines][DelayController.cleanupTestCoroutines] is called after every test case. +If this behavior is desirable, but some parts of the test still require accurate dispatching, for example, to ensure +that the code executes on the correct thread, then simply `launch` a new coroutine with the [StandardTestDispatcher]. ```kotlin -class TestClass { - private val testDispatcher = TestCoroutineDispatcher() - - @Before - fun setup() { - // provide the scope explicitly, in this example using a constructor parameter - Dispatchers.setMain(testDispatcher) - } - - @After - fun cleanUp() { - Dispatchers.resetMain() - testDispatcher.cleanupTestCoroutines() - } - - @Test - fun testFoo() = testDispatcher.runBlockingTest { - // TestCoroutineDispatcher.runBlockingTest uses `testDispatcher` to run coroutines - foo() +@Test +fun testEagerlyEnteringSomeChildCoroutines() = runTest(UnconfinedTestDispatcher()) { + var entered1 = false + launch { + entered1 = true } -} + assertTrue(entered1) // `entered1 = true` already executed -fun foo() { - MainScope().launch { - // launch will use the testDispatcher provided by setMain + var entered2 = false + launch(StandardTestDispatcher(testScheduler)) { + // this block and every coroutine launched inside it will explicitly go through the needed dispatches + entered2 = true } + assertFalse(entered2) + runCurrent() // need to explicitly run the dispatched continuation + assertTrue(entered2) } ``` -*Note:* Prefer to provide `TestCoroutineScope` when it does not complicate code since it will also elevate exceptions -to test failures. However, exposing a `CoroutineScope` to callers of a function may lead to complicated code, in which -case this is the preferred pattern. - -### Using `TestCoroutineScope` and `TestCoroutineDispatcher` without `runBlockingTest` +### Using `withTimeout` inside `runTest` -It is supported to use both [TestCoroutineScope] and [TestCoroutineDispatcher] without using the [runBlockingTest] -builder. Tests may need to do this in situations such as introducing multiple dispatchers and library writers may do -this to provide alternatives to `runBlockingTest`. +Timeouts are also susceptible to time control, so the code below will immediately finish. ```kotlin @Test -fun testFooWithAutoProgress() { - val scope = TestCoroutineScope() - scope.foo() - // foo is suspended waiting for time to progress - scope.advanceUntilIdle() - // foo's coroutine will be completed before here -} - -fun CoroutineScope.foo() { - launch { - println(1) // executes eagerly when foo() is called due to TestCoroutineScope - delay(1_000) // suspends until time is advanced by at least 1_000 - println(2) // executes after advanceTimeUntilIdle +fun testFooWithTimeout() = runTest { + assertFailsWith { + withTimeout(1_000) { + delay(999) + delay(2) + println("this won't be reached") + } } -} +} ``` -## Using time control with `withContext` - -Calls to `withContext(Dispatchers.IO)` or `withContext(Dispatchers.Default)` are common in coroutines based codebases. -Both dispatchers are not designed to interact with `TestCoroutineDispatcher`. +## Virtual time support with other dispatchers -Tests should provide a `TestCoroutineDispatcher` to replace these dispatchers if the `withContext` calls `delay` in the -function under test. For example, a test that calls `veryExpensiveOne` should provide a `TestCoroutineDispatcher` using -either dependency injection, a service locator, or a default parameter. +Calls to `withContext(Dispatchers.IO)`, `withContext(Dispatchers.Default)` ,and `withContext(Dispatchers.Main)` are +common in coroutines-based code bases. Unfortunately, just executing code in a test will not lead to these dispatchers +using the virtual time source, so delays will not be skipped in them. ```kotlin -suspend fun veryExpensiveOne() = withContext(Dispatchers.Default) { +suspend fun veryExpensiveFunction() = withContext(Dispatchers.Default) { delay(1_000) - 1 // for very expensive values of 1 + 1 } -``` - -In situations where the code inside the `withContext` is very simple, it is not as important to provide a test -dispatcher. The function `veryExpensiveTwo` will behave identically in a `TestCoroutineDispatcher` and -`Dispatchers.Default` after the thread switch for `Dispatchers.Default`. Because `withContext` always returns a value by -directly, there is no need to inject a `TestCoroutineDispatcher` into this function. -```kotlin -suspend fun veryExpensiveTwo() = withContext(Dispatchers.Default) { - 2 // for very expensive values of 2 +fun testExpensiveFunction() = runTest { + val result = veryExpensiveFunction() // will take a whole real-time second to execute + // the virtual time at this point is still 0 } ``` -Tests should provide a `TestCoroutineDispatcher` to code that calls `withContext` to provide time control for -delays, or when execution control is needed to test complex logic. - +Tests should, when possible, replace these dispatchers with a [TestDispatcher] if the `withContext` calls `delay` in the +function under test. For example, `veryExpensiveFunction` above should allow mocking with a [TestDispatcher] using +either dependency injection, a service locator, or a default parameter, if it is to be used with virtual time. ### Status of the API @@ -426,35 +362,32 @@ This API is experimental and it is may change before migrating out of experiment Changes during experimental may have deprecation applied when possible, but it is not advised to use the API in stable code before it leaves experimental due to possible breaking changes. -If you have any suggestions for improvements to this experimental API please share them them on the +If you have any suggestions for improvements to this experimental API please share them them on the [issue tracker](https://github.com/Kotlin/kotlinx.coroutines/issues). -[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html +[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html -[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html -[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html -[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html +[Dispatchers.Unconfined]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html +[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html [yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html -[CoroutineStart]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html -[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [ExperimentalCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-experimental-coroutines-api/index.html +[runTest]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-test.html +[TestCoroutineScheduler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scheduler/index.html +[TestScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-scope/index.html +[TestDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-dispatcher/index.html +[Dispatchers.setMain]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/set-main.html +[StandardTestDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-standard-test-dispatcher.html +[UnconfinedTestDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-unconfined-test-dispatcher.html [setMain]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/set-main.html -[runBlockingTest]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-blocking-test.html -[DelayController]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/index.html -[DelayController.advanceUntilIdle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/advance-until-idle.html -[DelayController.pauseDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/pause-dispatcher.html -[TestCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-dispatcher/index.html -[DelayController.resumeDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/resume-dispatcher.html -[TestCoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/index.html -[TestCoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-exception-handler/index.html -[TestCoroutineScope.cleanupTestCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/cleanup-test-coroutines.html -[DelayController.cleanupTestCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/cleanup-test-coroutines.html +[TestScope.testScheduler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-scope/test-scheduler.html +[TestScope.runTest]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-test.html +[runCurrent]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-current.html diff --git a/kotlinx-coroutines-test/common/src/TestDispatcher.kt b/kotlinx-coroutines-test/common/src/TestDispatcher.kt index c01e5b4d7b..3b756b19e9 100644 --- a/kotlinx-coroutines-test/common/src/TestDispatcher.kt +++ b/kotlinx-coroutines-test/common/src/TestDispatcher.kt @@ -12,7 +12,7 @@ import kotlin.jvm.* * A test dispatcher that can interface with a [TestCoroutineScheduler]. */ @ExperimentalCoroutinesApi -public sealed class TestDispatcher: CoroutineDispatcher(), Delay { +public abstract class TestDispatcher internal constructor(): CoroutineDispatcher(), Delay { /** The scheduler that this dispatcher is linked to. */ @ExperimentalCoroutinesApi public abstract val scheduler: TestCoroutineScheduler diff --git a/kotlinx-coroutines-test/common/src/TestDispatchers.kt b/kotlinx-coroutines-test/common/src/TestDispatchers.kt index 8e70050ebb..15b4dade84 100644 --- a/kotlinx-coroutines-test/common/src/TestDispatchers.kt +++ b/kotlinx-coroutines-test/common/src/TestDispatchers.kt @@ -13,6 +13,9 @@ import kotlin.jvm.* * Sets the given [dispatcher] as an underlying dispatcher of [Dispatchers.Main]. * All subsequent usages of [Dispatchers.Main] will use the given [dispatcher] under the hood. * + * Using [TestDispatcher] as an argument has special behavior: subsequently-called [runTest], as well as + * [TestScope] and test dispatcher constructors, will use the [TestCoroutineScheduler] of the provided dispatcher. + * * It is unsafe to call this method if alive coroutines launched in [Dispatchers.Main] exist. */ @ExperimentalCoroutinesApi diff --git a/kotlinx-coroutines-test/common/src/TestScope.kt b/kotlinx-coroutines-test/common/src/TestScope.kt index b48b273cd9..ffd5c01f7a 100644 --- a/kotlinx-coroutines-test/common/src/TestScope.kt +++ b/kotlinx-coroutines-test/common/src/TestScope.kt @@ -228,4 +228,10 @@ internal fun TestScope.asSpecificImplementation(): TestScopeImpl = when (this) { internal class UncaughtExceptionsBeforeTest : IllegalStateException( "There were uncaught exceptions in coroutines launched from TestScope before the test started. Please avoid this," + " as such exceptions are also reported in a platform-dependent manner so that they are not lost." -) \ No newline at end of file +) + +/** + * Thrown when a test has completed and there are tasks that are not completed or cancelled. + */ +@ExperimentalCoroutinesApi +internal class UncompletedCoroutinesError(message: String) : AssertionError(message) \ No newline at end of file diff --git a/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt b/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt index d3e4294a1a..203ddc4f11 100644 --- a/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt +++ b/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt @@ -311,8 +311,6 @@ class TestCoroutineSchedulerTest { private fun forTestDispatchers(block: (TestDispatcher) -> Unit): Unit = @Suppress("DEPRECATION") listOf( - TestCoroutineDispatcher(), - TestCoroutineDispatcher().also { it.pauseDispatcher() }, StandardTestDispatcher(), UnconfinedTestDispatcher() ).forEach { diff --git a/kotlinx-coroutines-test/common/src/migration/DelayController.kt b/kotlinx-coroutines-test/jvm/src/migration/DelayController.kt similarity index 92% rename from kotlinx-coroutines-test/common/src/migration/DelayController.kt rename to kotlinx-coroutines-test/jvm/src/migration/DelayController.kt index 62c2167177..e0701ae2cd 100644 --- a/kotlinx-coroutines-test/common/src/migration/DelayController.kt +++ b/kotlinx-coroutines-test/jvm/src/migration/DelayController.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.* "Use `TestCoroutineScheduler` to control virtual time.", level = DeprecationLevel.WARNING ) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public interface DelayController { /** * Returns the current virtual clock-time as it is known to this Dispatcher. @@ -106,6 +107,7 @@ public interface DelayController { "Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.", level = DeprecationLevel.WARNING ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public suspend fun pauseDispatcher(block: suspend () -> Unit) /** @@ -118,6 +120,7 @@ public interface DelayController { "Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.", level = DeprecationLevel.WARNING ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun pauseDispatcher() /** @@ -131,6 +134,7 @@ public interface DelayController { "Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.", level = DeprecationLevel.WARNING ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun resumeDispatcher() } @@ -143,6 +147,7 @@ internal interface SchedulerAsDelayController : DelayController { ReplaceWith("this.scheduler.currentTime"), level = DeprecationLevel.WARNING ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 override val currentTime: Long get() = scheduler.currentTime @@ -153,6 +158,7 @@ internal interface SchedulerAsDelayController : DelayController { ReplaceWith("this.scheduler.apply { advanceTimeBy(delayTimeMillis); runCurrent() }"), level = DeprecationLevel.WARNING ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 override fun advanceTimeBy(delayTimeMillis: Long): Long { val oldTime = scheduler.currentTime scheduler.advanceTimeBy(delayTimeMillis) @@ -166,6 +172,7 @@ internal interface SchedulerAsDelayController : DelayController { ReplaceWith("this.scheduler.advanceUntilIdle()"), level = DeprecationLevel.WARNING ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 override fun advanceUntilIdle(): Long { val oldTime = scheduler.currentTime scheduler.advanceUntilIdle() @@ -178,6 +185,7 @@ internal interface SchedulerAsDelayController : DelayController { ReplaceWith("this.scheduler.runCurrent()"), level = DeprecationLevel.WARNING ) + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 override fun runCurrent(): Unit = scheduler.runCurrent() /** @suppress */ diff --git a/kotlinx-coroutines-test/common/src/migration/TestBuildersDeprecated.kt b/kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt similarity index 93% rename from kotlinx-coroutines-test/common/src/migration/TestBuildersDeprecated.kt rename to kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt index 68398fb424..4524bf2867 100644 --- a/kotlinx-coroutines-test/common/src/migration/TestBuildersDeprecated.kt +++ b/kotlinx-coroutines-test/jvm/src/migration/TestBuildersDeprecated.kt @@ -47,6 +47,7 @@ import kotlin.jvm.* * @param testBody The code of the unit-test. */ @Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun runBlockingTest( context: CoroutineContext = EmptyCoroutineContext, testBody: suspend TestCoroutineScope.() -> Unit @@ -67,6 +68,7 @@ public fun runBlockingTest( * A version of [runBlockingTest] that works with [TestScope]. */ @Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun runBlockingTestOnTestScope( context: CoroutineContext = EmptyCoroutineContext, testBody: suspend TestScope.() -> Unit @@ -102,6 +104,7 @@ public fun runBlockingTestOnTestScope( * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope]. */ @Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit = runBlockingTest(coroutineContext, block) @@ -109,6 +112,7 @@ public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope. * Convenience method for calling [runBlockingTestOnTestScope] on an existing [TestScope]. */ @Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun TestScope.runBlockingTest(block: suspend TestScope.() -> Unit): Unit = runBlockingTestOnTestScope(coroutineContext, block) @@ -116,6 +120,7 @@ public fun TestScope.runBlockingTest(block: suspend TestScope.() -> Unit): Unit * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher]. */ @Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit = runBlockingTest(this, block) @@ -124,6 +129,7 @@ public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineS */ @ExperimentalCoroutinesApi @Deprecated("Use `runTest` instead.", level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun runTestWithLegacyScope( context: CoroutineContext = EmptyCoroutineContext, dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS, @@ -157,6 +163,7 @@ public fun runTestWithLegacyScope( */ @ExperimentalCoroutinesApi @Deprecated("Use `TestScope.runTest` instead.", level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun TestCoroutineScope.runTest( dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS, block: suspend TestCoroutineScope.() -> Unit diff --git a/kotlinx-coroutines-test/common/src/migration/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineDispatcher.kt similarity index 97% rename from kotlinx-coroutines-test/common/src/migration/TestCoroutineDispatcher.kt rename to kotlinx-coroutines-test/jvm/src/migration/TestCoroutineDispatcher.kt index 31249ee6e4..ec2a3046ee 100644 --- a/kotlinx-coroutines-test/common/src/migration/TestCoroutineDispatcher.kt +++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineDispatcher.kt @@ -24,6 +24,7 @@ import kotlin.coroutines.* @Deprecated("The execution order of `TestCoroutineDispatcher` can be confusing, and the mechanism of " + "pausing is typically misunderstood. Please use `StandardTestDispatcher` or `UnconfinedTestDispatcher` instead.", level = DeprecationLevel.WARNING) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public class TestCoroutineDispatcher(public override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler()): TestDispatcher(), Delay, SchedulerAsDelayController { diff --git a/kotlinx-coroutines-test/common/src/migration/TestCoroutineExceptionHandler.kt b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt similarity index 95% rename from kotlinx-coroutines-test/common/src/migration/TestCoroutineExceptionHandler.kt rename to kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt index f9991496a7..9da521f05c 100644 --- a/kotlinx-coroutines-test/common/src/migration/TestCoroutineExceptionHandler.kt +++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineExceptionHandler.kt @@ -18,6 +18,7 @@ import kotlin.coroutines.* "please report your use case at https://github.com/Kotlin/kotlinx.coroutines/issues.", level = DeprecationLevel.WARNING ) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public interface UncaughtExceptionCaptor { /** * List of uncaught coroutine exceptions. @@ -46,6 +47,7 @@ public interface UncaughtExceptionCaptor { "It may be to define one's own `CoroutineExceptionHandler` if you just need to handle '" + "uncaught exceptions without a special `TestCoroutineScope` integration.", level = DeprecationLevel.WARNING ) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public class TestCoroutineExceptionHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler, UncaughtExceptionCaptor { private val _exceptions = mutableListOf() diff --git a/kotlinx-coroutines-test/common/src/migration/TestCoroutineScope.kt b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt similarity index 95% rename from kotlinx-coroutines-test/common/src/migration/TestCoroutineScope.kt rename to kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt index 4a8b54ba69..45a3815681 100644 --- a/kotlinx-coroutines-test/common/src/migration/TestCoroutineScope.kt +++ b/kotlinx-coroutines-test/jvm/src/migration/TestCoroutineScope.kt @@ -14,6 +14,7 @@ import kotlin.coroutines.* */ @ExperimentalCoroutinesApi @Deprecated("Use `TestScope` in combination with `runTest` instead") +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public sealed interface TestCoroutineScope : CoroutineScope { /** * Called after the test completes. @@ -35,6 +36,8 @@ public sealed interface TestCoroutineScope : CoroutineScope { * @throws IllegalStateException if called more than once. */ @ExperimentalCoroutinesApi + @Deprecated("Please call `runTest`, which automatically performs the cleanup, instead of using this function.") + // Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun cleanupTestCoroutines() /** @@ -127,6 +130,7 @@ internal fun CoroutineContext.activeJobs(): Set { ), level = DeprecationLevel.WARNING ) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope { val scheduler = context[TestCoroutineScheduler] ?: TestCoroutineScheduler() return createTestCoroutineScope(TestCoroutineDispatcher(scheduler) + TestCoroutineExceptionHandler() + context) @@ -163,6 +167,7 @@ public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext) "Please use TestScope() construction instead, or just runTest(), without creating a scope.", level = DeprecationLevel.WARNING ) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope { val ctxWithDispatcher = context.withDelaySkipping() var scope: TestCoroutineScopeImpl? = null @@ -222,6 +227,7 @@ public val TestCoroutineScope.currentTime: Long ReplaceWith("this.testScheduler.apply { advanceTimeBy(delayTimeMillis); runCurrent() }"), DeprecationLevel.WARNING ) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun TestCoroutineScope.advanceTimeBy(delayTimeMillis: Long): Unit = when (val controller = coroutineContext.delayController) { null -> { @@ -265,6 +271,7 @@ public fun TestCoroutineScope.runCurrent() { ), DeprecationLevel.WARNING ) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public suspend fun TestCoroutineScope.pauseDispatcher(block: suspend () -> Unit) { delayControllerForPausing.pauseDispatcher(block) } @@ -280,6 +287,7 @@ public suspend fun TestCoroutineScope.pauseDispatcher(block: suspend () -> Unit) ), level = DeprecationLevel.WARNING ) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun TestCoroutineScope.pauseDispatcher() { delayControllerForPausing.pauseDispatcher() } @@ -295,6 +303,7 @@ public fun TestCoroutineScope.pauseDispatcher() { ), level = DeprecationLevel.WARNING ) +// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0 public fun TestCoroutineScope.resumeDispatcher() { delayControllerForPausing.resumeDispatcher() } @@ -321,9 +330,3 @@ public val TestCoroutineScope.uncaughtExceptions: List private val TestCoroutineScope.delayControllerForPausing: DelayController get() = coroutineContext.delayController ?: throw IllegalStateException("This scope isn't able to pause its dispatchers") - -/** - * Thrown when a test has completed and there are tasks that are not completed or cancelled. - */ -@ExperimentalCoroutinesApi -internal class UncompletedCoroutinesError(message: String) : AssertionError(message) diff --git a/kotlinx-coroutines-test/common/test/migration/RunBlockingTestOnTestScopeTest.kt b/kotlinx-coroutines-test/jvm/test/migration/RunBlockingTestOnTestScopeTest.kt similarity index 100% rename from kotlinx-coroutines-test/common/test/migration/RunBlockingTestOnTestScopeTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/RunBlockingTestOnTestScopeTest.kt diff --git a/kotlinx-coroutines-test/common/test/migration/RunTestLegacyScopeTest.kt b/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt similarity index 97% rename from kotlinx-coroutines-test/common/test/migration/RunTestLegacyScopeTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt index 3ea11139d1..a76263ddd2 100644 --- a/kotlinx-coroutines-test/common/test/migration/RunTestLegacyScopeTest.kt +++ b/kotlinx-coroutines-test/jvm/test/migration/RunTestLegacyScopeTest.kt @@ -79,7 +79,6 @@ class RunTestLegacyScopeTest { } @Test - @NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native fun testRunTestWithSmallTimeout() = testResultMap({ fn -> assertFailsWith { fn() } }) { @@ -100,7 +99,6 @@ class RunTestLegacyScopeTest { } @Test - @NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native fun testRunTestTimingOutAndThrowing() = testResultMap({ fn -> assertFailsWith { fn() } }) { @@ -276,4 +274,4 @@ class RunTestLegacyScopeTest { } } } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-test/common/test/migration/TestBuildersTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestBuildersTest.kt similarity index 100% rename from kotlinx-coroutines-test/common/test/migration/TestBuildersTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/TestBuildersTest.kt diff --git a/kotlinx-coroutines-test/common/test/migration/TestCoroutineDispatcherOrderTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherOrderTest.kt similarity index 100% rename from kotlinx-coroutines-test/common/test/migration/TestCoroutineDispatcherOrderTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherOrderTest.kt diff --git a/kotlinx-coroutines-test/common/test/migration/TestCoroutineDispatcherTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherTest.kt similarity index 100% rename from kotlinx-coroutines-test/common/test/migration/TestCoroutineDispatcherTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/TestCoroutineDispatcherTest.kt diff --git a/kotlinx-coroutines-test/common/test/migration/TestCoroutineExceptionHandlerTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineExceptionHandlerTest.kt similarity index 100% rename from kotlinx-coroutines-test/common/test/migration/TestCoroutineExceptionHandlerTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/TestCoroutineExceptionHandlerTest.kt diff --git a/kotlinx-coroutines-test/common/test/migration/TestCoroutineScopeTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestCoroutineScopeTest.kt similarity index 100% rename from kotlinx-coroutines-test/common/test/migration/TestCoroutineScopeTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/TestCoroutineScopeTest.kt diff --git a/kotlinx-coroutines-test/common/test/migration/TestRunBlockingOrderTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingOrderTest.kt similarity index 100% rename from kotlinx-coroutines-test/common/test/migration/TestRunBlockingOrderTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingOrderTest.kt diff --git a/kotlinx-coroutines-test/common/test/migration/TestRunBlockingTest.kt b/kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingTest.kt similarity index 100% rename from kotlinx-coroutines-test/common/test/migration/TestRunBlockingTest.kt rename to kotlinx-coroutines-test/jvm/test/migration/TestRunBlockingTest.kt