5
5
package kotlinx.coroutines.test
6
6
7
7
import kotlinx.coroutines.*
8
+ import kotlinx.coroutines.selects.*
8
9
import kotlin.coroutines.*
9
10
10
11
/* *
@@ -41,10 +42,10 @@ import kotlin.coroutines.*
41
42
* then they must implement [DelayController] and [TestCoroutineExceptionHandler] respectively.
42
43
* @param testBody The code of the unit-test.
43
44
*/
44
- @ExperimentalCoroutinesApi
45
+ @Deprecated( " Use `runTest` instead to support completing from other dispatchers. " , level = DeprecationLevel . WARNING )
45
46
public fun runBlockingTest (context : CoroutineContext = EmptyCoroutineContext , testBody : suspend TestCoroutineScope .() -> Unit ) {
46
- val scope = TestCoroutineScope (context)
47
- val scheduler = scope.coroutineContext[ TestCoroutineScheduler ] !!
47
+ val scope = TestCoroutineScope (TestCoroutineDispatcher () + SupervisorJob () + context)
48
+ val scheduler = scope.testScheduler
48
49
val deferred = scope.async {
49
50
scope.testBody()
50
51
}
@@ -59,13 +60,260 @@ public fun runBlockingTest(context: CoroutineContext = EmptyCoroutineContext, te
59
60
* Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope].
60
61
*/
61
62
// todo: need documentation on how this extension is supposed to be used
62
- @ExperimentalCoroutinesApi
63
+ @Deprecated( " Use `runTest` instead to support completing from other dispatchers. " , level = DeprecationLevel . WARNING )
63
64
public fun TestCoroutineScope.runBlockingTest (block : suspend TestCoroutineScope .() -> Unit ): Unit =
64
65
runBlockingTest(coroutineContext, block)
65
66
66
67
/* *
67
68
* Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
68
69
*/
69
- @ExperimentalCoroutinesApi
70
+ @Deprecated( " Use `runTest` instead to support completing from other dispatchers. " , level = DeprecationLevel . WARNING )
70
71
public fun TestCoroutineDispatcher.runBlockingTest (block : suspend TestCoroutineScope .() -> Unit ): Unit =
71
72
runBlockingTest(this , block)
73
+
74
+ /* *
75
+ * A test result.
76
+ *
77
+ * * On JVM and Native, this resolves to [Unit], representing the fact that tests are run in a blocking manner on these
78
+ * platforms: a call to a function returning a [TestResult] will simply execute the test inside it.
79
+ * * On JS, this is a `Promise`, which reflects the fact that the test-running function does not wait for a test to
80
+ * finish. The JS test frameworks typically support returning `Promise` from a test and will correctly handle it.
81
+ *
82
+ * Because of the behavior on JS, extra care must be taken when writing multiplatform tests to avoid losing test errors:
83
+ * * Don't do anything after running the functions returning a [TestResult]. On JS, this code will execute *before* the
84
+ * test finishes.
85
+ * * As a corollary, don't run functions returning a [TestResult] more than once per test. The only valid thing to do
86
+ * with a [TestResult] is to immediately `return` it from a test.
87
+ * * Don't nest functions returning a [TestResult].
88
+ */
89
+ @Suppress(" NO_ACTUAL_FOR_EXPECT" )
90
+ @ExperimentalCoroutinesApi
91
+ public expect class TestResult
92
+
93
+ /* *
94
+ * Executes [testBody] as a test in a new coroutine, returning [TestResult].
95
+ *
96
+ * On JVM and Native, this function behaves similarly to [runBlocking], with the difference that the code that it runs
97
+ * will skip delays. This allows to use [delay] in without causing the tests to take more time than necessary.
98
+ * On JS, this function creates a `Promise` that executes the test body with the delay-skipping behavior.
99
+ *
100
+ * ```
101
+ * @Test
102
+ * fun exampleTest() = runTest {
103
+ * val deferred = async {
104
+ * delay(1_000)
105
+ * async {
106
+ * delay(1_000)
107
+ * }.await()
108
+ * }
109
+ *
110
+ * deferred.await() // result available immediately
111
+ * }
112
+ * ```
113
+ *
114
+ * The platform difference entails that, in order to use this function correctly in common code, one must always
115
+ * immediately return the produced [TestResult] from the test method, without doing anything else afterwards. See
116
+ * [TestResult] for details on this.
117
+ *
118
+ * The test is run in a single thread, unless other [ContinuationInterceptor] are used for child coroutines.
119
+ * Because of this, child coroutines are not executed in parallel to the test body.
120
+ * In order to for the spawned-off asynchronous code to actually be executed, one must either [yield] or suspend the
121
+ * test body some other way, or use commands that control scheduling (see [TestCoroutineScheduler]).
122
+ *
123
+ * ```
124
+ * @Test
125
+ * fun exampleWaitingForAsyncTasks1() = runTest {
126
+ * // 1
127
+ * val job = launch {
128
+ * // 3
129
+ * }
130
+ * // 2
131
+ * job.join() // the main test coroutine suspends here, so the child is executed
132
+ * // 4
133
+ * }
134
+ *
135
+ * @Test
136
+ * fun exampleWaitingForAsyncTasks2() = runTest {
137
+ * // 1
138
+ * launch {
139
+ * // 3
140
+ * }
141
+ * // 2
142
+ * advanceUntilIdle() // runs the tasks until their queue is empty
143
+ * // 4
144
+ * }
145
+ * ```
146
+ *
147
+ * ### Task scheduling
148
+ *
149
+ * Delay-skipping is achieved by using virtual time. [TestCoroutineScheduler] is automatically created (if it wasn't
150
+ * passed in some way in [context]) and can be used to control the virtual time, advancing it, running the tasks
151
+ * scheduled at a specific time etc. Some convenience methods are available on [TestCoroutineScope] to control the
152
+ * scheduler.
153
+ *
154
+ * Delays in code that runs inside dispatchers that don't use a [TestCoroutineScheduler] don't get skipped:
155
+ * ```
156
+ * @Test
157
+ * fun exampleTest() = runTest {
158
+ * val elapsed = TimeSource.Monotonic.measureTime {
159
+ * val deferred = async {
160
+ * delay(1_000) // will be skipped
161
+ * withContext(Dispatchers.Default) {
162
+ * delay(5_000) // Dispatchers.Default doesn't know about TestCoroutineScheduler
163
+ * }
164
+ * }
165
+ * deferred.await()
166
+ * }
167
+ * println(elapsed) // about five seconds
168
+ * }
169
+ * ```
170
+ *
171
+ * ### Failures
172
+ *
173
+ * #### Test body failures
174
+ *
175
+ * If the created coroutine completes with an exception, then this exception will be thrown at the end of the test.
176
+ *
177
+ * #### Reported exceptions
178
+ *
179
+ * Unhandled exceptions will be thrown at the end of the test.
180
+ * If the uncaught exceptions happen after the test finishes, the error is propagated in a platform-specific manner.
181
+ * If the test coroutine completes with an exception, the unhandled exceptions are suppressed by it.
182
+ *
183
+ * #### Uncompleted coroutines
184
+ *
185
+ * This method requires that, after the test coroutine has completed, all the other coroutines launched inside
186
+ * [testBody] also complete, or are cancelled.
187
+ * Otherwise, the test will be failed (which, on JVM and Native, means that [runTest] itself will throw
188
+ * [AssertionError], whereas on JS, the `Promise` will fail with it).
189
+ *
190
+ * In the general case, if there are active jobs, it's impossible to detect if they are going to complete eventually due
191
+ * to the asynchronous nature of coroutines. In order to prevent tests hanging in this scenario, [runTest] will wait
192
+ * for [dispatchTimeoutMs] milliseconds (by default, 60 seconds) from the moment when [TestCoroutineScheduler] becomes
193
+ * idle before throwing [AssertionError]. If some dispatcher linked to [TestCoroutineScheduler] receives a
194
+ * task during that time, the timer gets reset.
195
+ *
196
+ * ### Configuration
197
+ *
198
+ * [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine
199
+ * scope created for the test, [context] also can be used to change how the test is executed.
200
+ * See the [TestCoroutineScope] constructor documentation for details.
201
+ *
202
+ * @throws IllegalArgumentException if the [context] is invalid. See the [TestCoroutineScope] constructor docs for
203
+ * details.
204
+ */
205
+ @ExperimentalCoroutinesApi
206
+ public fun runTest (
207
+ context : CoroutineContext = EmptyCoroutineContext ,
208
+ dispatchTimeoutMs : Long = DEFAULT_DISPATCH_TIMEOUT_MS ,
209
+ testBody : suspend TestCoroutineScope .() -> Unit
210
+ ): TestResult {
211
+ if (context[RunningInRunTest ] != null )
212
+ throw IllegalStateException (" Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details." )
213
+ val testScope = TestBodyCoroutine <Unit >(TestCoroutineScope (context + RunningInRunTest ))
214
+ val scheduler = testScope.testScheduler
215
+ return createTestResult {
216
+ /* * TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on Native with
217
+ * [TestCoroutineDispatcher], because the event loop is not started. */
218
+ testScope.start(CoroutineStart .DEFAULT , testScope) {
219
+ testBody()
220
+ }
221
+ var completed = false
222
+ while (! completed) {
223
+ scheduler.advanceUntilIdle()
224
+ if (testScope.isCompleted) {
225
+ /* don't even enter `withTimeout`; this allows to use a timeout of zero to check that there are no
226
+ non-trivial dispatches. */
227
+ completed = true
228
+ continue
229
+ }
230
+ select<Unit > {
231
+ testScope.onJoin {
232
+ completed = true
233
+ }
234
+ scheduler.onDispatchEvent {
235
+ // we received knowledge that `scheduler` observed a dispatch event, so we reset the timeout
236
+ }
237
+ onTimeout(dispatchTimeoutMs) {
238
+ try {
239
+ testScope.cleanupTestCoroutines()
240
+ } catch (e: UncompletedCoroutinesError ) {
241
+ // we expect these and will instead throw a more informative exception just below.
242
+ }
243
+ throw UncompletedCoroutinesError (" The test coroutine was not completed after waiting for $dispatchTimeoutMs ms" )
244
+ }
245
+ }
246
+ }
247
+ testScope.getCompletionExceptionOrNull()?.let {
248
+ try {
249
+ testScope.cleanupTestCoroutines()
250
+ } catch (e: UncompletedCoroutinesError ) {
251
+ // it's normal that some jobs are not completed if the test body has failed, won't clutter the output
252
+ } catch (e: Throwable ) {
253
+ it.addSuppressed(e)
254
+ }
255
+ throw it
256
+ }
257
+ testScope.cleanupTestCoroutines()
258
+ }
259
+ }
260
+
261
+ /* *
262
+ * Runs [testProcedure], creating a [TestResult].
263
+ */
264
+ @Suppress(" NO_ACTUAL_FOR_EXPECT" ) // actually suppresses `TestResult`
265
+ internal expect fun createTestResult (testProcedure : suspend () -> Unit ): TestResult
266
+
267
+ /* *
268
+ * Runs a test in a [TestCoroutineScope] based on this one.
269
+ *
270
+ * Calls [runTest] using a coroutine context from this [TestCoroutineScope]. The [TestCoroutineScope] used to run
271
+ * [block] will be different from this one, but will use its [Job] as a parent.
272
+ *
273
+ * Since this function returns [TestResult], in order to work correctly on the JS, its result must be returned
274
+ * immediately from the test body. See the docs for [TestResult] for details.
275
+ */
276
+ @ExperimentalCoroutinesApi
277
+ public fun TestCoroutineScope.runTest (
278
+ dispatchTimeoutMs : Long = DEFAULT_DISPATCH_TIMEOUT_MS ,
279
+ block : suspend TestCoroutineScope .() -> Unit
280
+ ): TestResult =
281
+ runTest(coroutineContext, dispatchTimeoutMs, block)
282
+
283
+ /* *
284
+ * Run a test using this [TestDispatcher].
285
+ *
286
+ * A convenience function that calls [runTest] with the given arguments.
287
+ *
288
+ * Since this function returns [TestResult], in order to work correctly on the JS, its result must be returned
289
+ * immediately from the test body. See the docs for [TestResult] for details.
290
+ */
291
+ @ExperimentalCoroutinesApi
292
+ public fun TestDispatcher.runTest (
293
+ dispatchTimeoutMs : Long = DEFAULT_DISPATCH_TIMEOUT_MS ,
294
+ block : suspend TestCoroutineScope .() -> Unit
295
+ ): TestResult =
296
+ runTest(this , dispatchTimeoutMs, block)
297
+
298
+ /* * A coroutine context element indicating that the coroutine is running inside `runTest`. */
299
+ private object RunningInRunTest: CoroutineContext.Key<RunningInRunTest>, CoroutineContext.Element {
300
+ override val key: CoroutineContext .Key <* >
301
+ get() = this
302
+
303
+ override fun toString (): String = " RunningInRunTest"
304
+ }
305
+
306
+ /* * The default timeout to use when waiting for asynchronous completions of the coroutines managed by
307
+ * a [TestCoroutineScheduler]. */
308
+ private const val DEFAULT_DISPATCH_TIMEOUT_MS = 60_000L
309
+
310
+ private class TestBodyCoroutine <T >(
311
+ private val testScope : TestCoroutineScope ,
312
+ ) : AbstractCoroutine<T>(testScope.coroutineContext, initParentJob = true , active = true ), TestCoroutineScope,
313
+ UncaughtExceptionCaptor by testScope.coroutineContext.uncaughtExceptionCaptor
314
+ {
315
+ override val testScheduler get() = testScope.testScheduler
316
+
317
+ override fun cleanupTestCoroutines () = testScope.cleanupTestCoroutines()
318
+
319
+ }
0 commit comments