@@ -10,6 +10,8 @@ import kotlinx.coroutines.*
10
10
import kotlinx.coroutines.selects.*
11
11
import kotlin.coroutines.*
12
12
import kotlin.jvm.*
13
+ import kotlin.time.*
14
+ import kotlin.time.Duration.Companion.milliseconds
13
15
14
16
/* *
15
17
* A test result.
@@ -41,9 +43,9 @@ public expect class TestResult
41
43
* @Test
42
44
* fun exampleTest() = runTest {
43
45
* val deferred = async {
44
- * delay(1_000 )
46
+ * delay(1.seconds )
45
47
* async {
46
- * delay(1_000 )
48
+ * delay(1.seconds )
47
49
* }.await()
48
50
* }
49
51
*
@@ -99,9 +101,9 @@ public expect class TestResult
99
101
* fun exampleTest() = runTest {
100
102
* val elapsed = TimeSource.Monotonic.measureTime {
101
103
* val deferred = async {
102
- * delay(1_000 ) // will be skipped
104
+ * delay(1.seconds ) // will be skipped
103
105
* withContext(Dispatchers.Default) {
104
- * delay(5_000 ) // Dispatchers.Default doesn't know about TestCoroutineScheduler
106
+ * delay(5.seconds ) // Dispatchers.Default doesn't know about TestCoroutineScheduler
105
107
* }
106
108
* }
107
109
* deferred.await()
@@ -131,7 +133,7 @@ public expect class TestResult
131
133
*
132
134
* In the general case, if there are active jobs, it's impossible to detect if they are going to complete eventually due
133
135
* to the asynchronous nature of coroutines. In order to prevent tests hanging in this scenario, [runTest] will wait
134
- * for [dispatchTimeoutMs] milliseconds (by default, 60 seconds) from the moment when [TestCoroutineScheduler] becomes
136
+ * for [dispatchTimeout] (by default, 60 seconds) from the moment when [TestCoroutineScheduler] becomes
135
137
* idle before throwing [AssertionError]. If some dispatcher linked to [TestCoroutineScheduler] receives a
136
138
* task during that time, the timer gets reset.
137
139
*
@@ -146,32 +148,168 @@ public expect class TestResult
146
148
@ExperimentalCoroutinesApi
147
149
public fun runTest (
148
150
context : CoroutineContext = EmptyCoroutineContext ,
149
- dispatchTimeoutMs : Long = DEFAULT_DISPATCH_TIMEOUT_MS ,
151
+ dispatchTimeout : Duration = DEFAULT_DISPATCH_TIMEOUT ,
150
152
testBody : suspend TestScope .() -> Unit
151
153
): TestResult {
152
154
if (context[RunningInRunTest ] != null )
153
155
throw IllegalStateException (" Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details." )
154
- return TestScope (context + RunningInRunTest ).runTest(dispatchTimeoutMs , testBody)
156
+ return TestScope (context + RunningInRunTest ).runTest(dispatchTimeout , testBody)
155
157
}
156
158
159
+ /* *
160
+ * Executes [testBody] as a test in a new coroutine, returning [TestResult].
161
+ *
162
+ * On JVM and Native, this function behaves similarly to `runBlocking`, with the difference that the code that it runs
163
+ * will skip delays. This allows to use [delay] in without causing the tests to take more time than necessary.
164
+ * On JS, this function creates a `Promise` that executes the test body with the delay-skipping behavior.
165
+ *
166
+ * ```
167
+ * @Test
168
+ * fun exampleTest() = runTest {
169
+ * val deferred = async {
170
+ * delay(1.seconds)
171
+ * async {
172
+ * delay(1.seconds)
173
+ * }.await()
174
+ * }
175
+ *
176
+ * deferred.await() // result available immediately
177
+ * }
178
+ * ```
179
+ *
180
+ * The platform difference entails that, in order to use this function correctly in common code, one must always
181
+ * immediately return the produced [TestResult] from the test method, without doing anything else afterwards. See
182
+ * [TestResult] for details on this.
183
+ *
184
+ * The test is run in a single thread, unless other [CoroutineDispatcher] are used for child coroutines.
185
+ * Because of this, child coroutines are not executed in parallel to the test body.
186
+ * In order for the spawned-off asynchronous code to actually be executed, one must either [yield] or suspend the
187
+ * test body some other way, or use commands that control scheduling (see [TestCoroutineScheduler]).
188
+ *
189
+ * ```
190
+ * @Test
191
+ * fun exampleWaitingForAsyncTasks1() = runTest {
192
+ * // 1
193
+ * val job = launch {
194
+ * // 3
195
+ * }
196
+ * // 2
197
+ * job.join() // the main test coroutine suspends here, so the child is executed
198
+ * // 4
199
+ * }
200
+ *
201
+ * @Test
202
+ * fun exampleWaitingForAsyncTasks2() = runTest {
203
+ * // 1
204
+ * launch {
205
+ * // 3
206
+ * }
207
+ * // 2
208
+ * advanceUntilIdle() // runs the tasks until their queue is empty
209
+ * // 4
210
+ * }
211
+ * ```
212
+ *
213
+ * ### Task scheduling
214
+ *
215
+ * Delay-skipping is achieved by using virtual time.
216
+ * If [Dispatchers.Main] is set to a [TestDispatcher] via [Dispatchers.setMain] before the test,
217
+ * then its [TestCoroutineScheduler] is used;
218
+ * otherwise, a new one is automatically created (or taken from [context] in some way) and can be used to control
219
+ * the virtual time, advancing it, running the tasks scheduled at a specific time etc.
220
+ * Some convenience methods are available on [TestScope] to control the scheduler.
221
+ *
222
+ * Delays in code that runs inside dispatchers that don't use a [TestCoroutineScheduler] don't get skipped:
223
+ * ```
224
+ * @Test
225
+ * fun exampleTest() = runTest {
226
+ * val elapsed = TimeSource.Monotonic.measureTime {
227
+ * val deferred = async {
228
+ * delay(1.seconds) // will be skipped
229
+ * withContext(Dispatchers.Default) {
230
+ * delay(5.seconds) // Dispatchers.Default doesn't know about TestCoroutineScheduler
231
+ * }
232
+ * }
233
+ * deferred.await()
234
+ * }
235
+ * println(elapsed) // about five seconds
236
+ * }
237
+ * ```
238
+ *
239
+ * ### Failures
240
+ *
241
+ * #### Test body failures
242
+ *
243
+ * If the created coroutine completes with an exception, then this exception will be thrown at the end of the test.
244
+ *
245
+ * #### Reported exceptions
246
+ *
247
+ * Unhandled exceptions will be thrown at the end of the test.
248
+ * If the uncaught exceptions happen after the test finishes, the error is propagated in a platform-specific manner.
249
+ * If the test coroutine completes with an exception, the unhandled exceptions are suppressed by it.
250
+ *
251
+ * #### Uncompleted coroutines
252
+ *
253
+ * This method requires that, after the test coroutine has completed, all the other coroutines launched inside
254
+ * [testBody] also complete, or are cancelled.
255
+ * Otherwise, the test will be failed (which, on JVM and Native, means that [runTest] itself will throw
256
+ * [AssertionError], whereas on JS, the `Promise` will fail with it).
257
+ *
258
+ * In the general case, if there are active jobs, it's impossible to detect if they are going to complete eventually due
259
+ * to the asynchronous nature of coroutines. In order to prevent tests hanging in this scenario, [runTest] will wait
260
+ * for [dispatchTimeoutMs] from the moment when [TestCoroutineScheduler] becomes
261
+ * idle before throwing [AssertionError]. If some dispatcher linked to [TestCoroutineScheduler] receives a
262
+ * task during that time, the timer gets reset.
263
+ *
264
+ * ### Configuration
265
+ *
266
+ * [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine
267
+ * scope created for the test, [context] also can be used to change how the test is executed.
268
+ * See the [TestScope] constructor function documentation for details.
269
+ *
270
+ * @throws IllegalArgumentException if the [context] is invalid. See the [TestScope] constructor docs for details.
271
+ */
272
+ @ExperimentalCoroutinesApi
273
+ public fun runTest (
274
+ context : CoroutineContext = EmptyCoroutineContext ,
275
+ dispatchTimeoutMs : Long ,
276
+ testBody : suspend TestScope .() -> Unit
277
+ ): TestResult = runTest(
278
+ context = context,
279
+ dispatchTimeout = dispatchTimeoutMs.milliseconds,
280
+ testBody = testBody
281
+ )
282
+
157
283
/* *
158
284
* Performs [runTest] on an existing [TestScope].
159
285
*/
160
286
@ExperimentalCoroutinesApi
161
287
public fun TestScope.runTest (
162
- dispatchTimeoutMs : Long = DEFAULT_DISPATCH_TIMEOUT_MS ,
288
+ dispatchTimeout : Duration ,
163
289
testBody : suspend TestScope .() -> Unit
164
290
): TestResult = asSpecificImplementation().let {
165
291
it.enter()
166
292
createTestResult {
167
- runTestCoroutine(it, dispatchTimeoutMs , TestScopeImpl ::tryGetCompletionCause, testBody) {
293
+ runTestCoroutine(it, dispatchTimeout , TestScopeImpl ::tryGetCompletionCause, testBody) {
168
294
backgroundScope.cancel()
169
295
testScheduler.advanceUntilIdleOr { false }
170
296
it.leave()
171
297
}
172
298
}
173
299
}
174
300
301
+ /* *
302
+ * Performs [runTest] on an existing [TestScope].
303
+ */
304
+ @ExperimentalCoroutinesApi
305
+ public fun TestScope.runTest (
306
+ dispatchTimeoutMs : Long = DEFAULT_DISPATCH_TIMEOUT_MS ,
307
+ testBody : suspend TestScope .() -> Unit
308
+ ): TestResult = runTest(
309
+ dispatchTimeout = dispatchTimeoutMs.milliseconds,
310
+ testBody = testBody
311
+ )
312
+
175
313
/* *
176
314
* Runs [testProcedure], creating a [TestResult].
177
315
*/
@@ -189,10 +327,11 @@ internal object RunningInRunTest : CoroutineContext.Key<RunningInRunTest>, Corou
189
327
/* * The default timeout to use when waiting for asynchronous completions of the coroutines managed by
190
328
* a [TestCoroutineScheduler]. */
191
329
internal const val DEFAULT_DISPATCH_TIMEOUT_MS = 60_000L
330
+ internal val DEFAULT_DISPATCH_TIMEOUT = DEFAULT_DISPATCH_TIMEOUT_MS .milliseconds
192
331
193
332
/* *
194
333
* Run the [body][testBody] of the [test coroutine][coroutine], waiting for asynchronous completions for at most
195
- * [dispatchTimeoutMs] milliseconds , and performing the [cleanup] procedure at the end.
334
+ * [dispatchTimeout] , and performing the [cleanup] procedure at the end.
196
335
*
197
336
* [tryGetCompletionCause] is the [JobSupport.completionCause], which is passed explicitly because it is protected.
198
337
*
@@ -201,7 +340,7 @@ internal const val DEFAULT_DISPATCH_TIMEOUT_MS = 60_000L
201
340
*/
202
341
internal suspend fun <T : AbstractCoroutine <Unit >> CoroutineScope.runTestCoroutine (
203
342
coroutine : T ,
204
- dispatchTimeoutMs : Long ,
343
+ dispatchTimeout : Duration ,
205
344
tryGetCompletionCause : T .() -> Throwable ? ,
206
345
testBody : suspend T .() -> Unit ,
207
346
cleanup : () -> List <Throwable >,
@@ -258,8 +397,8 @@ internal suspend fun <T: AbstractCoroutine<Unit>> CoroutineScope.runTestCoroutin
258
397
scheduler.onDispatchEvent {
259
398
// we received knowledge that `scheduler` observed a dispatch event, so we reset the timeout
260
399
}
261
- onTimeout(dispatchTimeoutMs ) {
262
- handleTimeout(coroutine, dispatchTimeoutMs , tryGetCompletionCause, cleanup)
400
+ onTimeout(dispatchTimeout ) {
401
+ handleTimeout(coroutine, dispatchTimeout , tryGetCompletionCause, cleanup)
263
402
}
264
403
}
265
404
} finally {
@@ -284,7 +423,7 @@ internal suspend fun <T: AbstractCoroutine<Unit>> CoroutineScope.runTestCoroutin
284
423
*/
285
424
private inline fun <T : AbstractCoroutine <Unit >> handleTimeout (
286
425
coroutine : T ,
287
- dispatchTimeoutMs : Long ,
426
+ dispatchTimeout : Duration ,
288
427
tryGetCompletionCause : T .() -> Throwable ? ,
289
428
cleanup : () -> List <Throwable >,
290
429
) {
@@ -296,7 +435,7 @@ private inline fun<T: AbstractCoroutine<Unit>> handleTimeout(
296
435
}
297
436
val activeChildren = coroutine.children.filter { it.isActive }.toList()
298
437
val completionCause = if (coroutine.isCancelled) coroutine.tryGetCompletionCause() else null
299
- var message = " After waiting for $dispatchTimeoutMs ms "
438
+ var message = " After waiting for $dispatchTimeout "
300
439
if (completionCause == null )
301
440
message + = " , the test coroutine is not completing"
302
441
if (activeChildren.isNotEmpty())
0 commit comments