1
1
package kotlinx.coroutines.test
2
2
3
3
import kotlinx.coroutines.*
4
- import java.util.concurrent.TimeoutException
5
4
import kotlin.coroutines.ContinuationInterceptor
6
5
import kotlin.coroutines.CoroutineContext
7
- import kotlin.coroutines.coroutineContext
8
6
9
- /* *
10
- * Executes a [testBody] in a [TestCoroutineScope] which provides detailed control over the execution of coroutines.
11
- *
12
- * This function should be used when you need detailed control over the execution of your test. For most tests consider
13
- * using [runBlockingTest].
14
- *
15
- * Code executed in a `asyncTest` will dispatch lazily. That means calling builders such as [launch] or [async] will
16
- * not execute the block immediately. You can use methods like [TestCoroutineScope.runCurrent] and
17
- * [TestCoroutineScope.advanceTimeTo] on the [TestCoroutineScope]. For a full list of execution methods see
18
- * [DelayController].
19
- *
20
- * ```
21
- * @Test
22
- * fun exampleTest() = asyncTest {
23
- * // 1: launch will execute but not run the body
24
- * launch {
25
- * // 3: the body of launch will execute in response to runCurrent [currentTime = 0ms]
26
- * delay(1_000)
27
- * // 5: After the time is advanced, delay(1_000) will return [currentTime = 1000ms]
28
- * println("Faster delays!")
29
- * }
30
- *
31
- * // 2: use runCurrent() to execute the body of launch [currentTime = 0ms]
32
- * runCurrent()
33
- *
34
- * // 4: advance the dispatcher "time" by 1_000, which will resume after the delay
35
- * advanceTimeTo(1_000)
36
- *
37
- * ```
38
- *
39
- * This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test
40
- * conditions.
41
- *
42
- * In addition any unhandled exceptions thrown in coroutines must be rethrown by
43
- * [TestCoroutineScope.rethrowUncaughtCoroutineException] or cleared via [TestCoroutineScope.exceptions] inside of
44
- * [testBody].
45
- *
46
- * @throws UncompletedCoroutinesError If the [testBody] does not complete (or cancel) all coroutines that it launches
47
- * (including coroutines suspended on await).
48
- * @throws Throwable If an uncaught exception was captured by this test it will be rethrown.
49
- *
50
- * @param context An optional dispatcher, during [testBody] execution [DelayController.dispatchImmediately] will be set to false
51
- * @param testBody The code of the unit-test.
52
- *
53
- * @see [runBlockingTest]
54
- */
55
- fun asyncTest (context : CoroutineContext ? = null, testBody : TestCoroutineScope .() -> Unit ) {
56
- val (safeContext, dispatcher) = context.checkArguments()
57
- // smart cast dispatcher to expose interface
58
- dispatcher as DelayController
59
- val scope = TestCoroutineScope (safeContext)
60
-
61
- val oldDispatch = dispatcher.dispatchImmediately
62
- dispatcher.dispatchImmediately = false
63
-
64
- try {
65
- scope.testBody()
66
- scope.cleanupTestCoroutines()
67
-
68
- // check for any active child jobs after cleanup (e.g. coroutines suspended on calls to await)
69
- val job = checkNotNull(safeContext[Job ]) { " Job required for asyncTest" }
70
- val activeChildren = job.children.filter { it.isActive }.toList()
71
- if (activeChildren.isNotEmpty()) {
72
- throw UncompletedCoroutinesError (" Test finished with active jobs: ${activeChildren} " )
73
- }
74
- } finally {
75
- dispatcher.dispatchImmediately = oldDispatch
76
- }
77
- }
78
-
79
- /* *
80
- * @see [asyncTest]
81
- */
82
- fun TestCoroutineScope.asyncTest (testBody : TestCoroutineScope .() -> Unit ) =
83
- asyncTest(coroutineContext, testBody)
84
-
85
- /* *
86
- * This method is deprecated.
87
- *
88
- * @see [cleanupTestCoroutines]
89
- */
90
- @Deprecated(" This API has been deprecated to integrate with Structured Concurrency." ,
91
- ReplaceWith (" scope.runBlockingTest(testBody)" , " kotlinx.coroutines.test" ),
92
- level = DeprecationLevel .ERROR )
93
- fun withTestContext (scope : TestCoroutineScope , testBody : suspend TestCoroutineScope .() -> Unit ) {
94
- scope.runBlockingTest(testBody)
95
- }
96
7
97
8
/* *
98
9
* Executes a [testBody] inside an immediate execution dispatcher.
99
10
*
100
11
* This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks.
101
12
* You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take
102
13
* extra time.
103
- *
104
- * Compared to [asyncTest], it provides a smaller API for tests that don't need detailed control over execution.
105
- *
14
+ **
106
15
* ```
107
16
* @Test
108
17
* fun exampleTest() = runBlockingTest {
@@ -121,43 +30,39 @@ fun withTestContext(scope: TestCoroutineScope, testBody: suspend TestCoroutineSc
121
30
* This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test
122
31
* conditions.
123
32
*
124
- * In unhandled exceptions inside coroutines will not fail the test.
33
+ * Unhandled exceptions thrown by coroutines in the test will be re-thrown at the end of the test.
125
34
*
126
35
* @throws UncompletedCoroutinesError If the [testBody] does not complete (or cancel) all coroutines that it launches
127
- * (including coroutines suspended on await).
36
+ * (including coroutines suspended on join/ await).
128
37
*
129
- * @param context An optional context, during [testBody] execution [DelayController.dispatchImmediately] will be set to true
38
+ * @param context An optional context that MAY contain a [DelayController] and/or [TestCoroutineExceptionHandler]
130
39
* @param testBody The code of the unit-test.
131
- *
132
- * @see [asyncTest]
133
40
*/
134
41
fun runBlockingTest (context : CoroutineContext ? = null, testBody : suspend TestCoroutineScope .() -> Unit ) {
135
42
val (safeContext, dispatcher) = context.checkArguments()
136
43
// smart cast dispatcher to expose interface
137
44
dispatcher as DelayController
138
45
139
- val oldDispatch = dispatcher.dispatchImmediately
140
- dispatcher.dispatchImmediately = true
141
- val scope = TestCoroutineScope (safeContext)
142
- try {
46
+ val startingJobs = safeContext.activeJobs()
143
47
144
- val deferred = scope.async {
145
- scope.testBody()
146
- }
147
- dispatcher.advanceUntilIdle()
148
- deferred.getCompletionExceptionOrNull()?.let {
149
- throw it
150
- }
151
- scope.cleanupTestCoroutines()
152
- val activeChildren = checkNotNull(safeContext[Job ]).children.filter { it.isActive }.toList()
153
- if (activeChildren.isNotEmpty()) {
154
- throw UncompletedCoroutinesError (" Test finished with active jobs: ${activeChildren} " )
155
- }
156
- } finally {
157
- dispatcher.dispatchImmediately = oldDispatch
48
+ val scope = TestCoroutineScope (safeContext)
49
+ val deferred = scope.async {
50
+ scope.testBody()
51
+ }
52
+ dispatcher.advanceUntilIdle()
53
+ deferred.getCompletionExceptionOrNull()?.let {
54
+ throw it
55
+ }
56
+ scope.cleanupTestCoroutines()
57
+ val endingJobs = safeContext.activeJobs()
58
+ if ((endingJobs - startingJobs).isNotEmpty()) {
59
+ throw UncompletedCoroutinesError (" Test finished with active jobs: $endingJobs " )
158
60
}
159
61
}
160
62
63
+ private fun CoroutineContext.activeJobs () =
64
+ checkNotNull(this [Job ]).children.filter { it.isActive }.toSet()
65
+
161
66
/* *
162
67
* Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope].
163
68
*/
@@ -194,4 +99,16 @@ private fun CoroutineContext?.checkArguments(): Pair<CoroutineContext, Continuat
194
99
195
100
safeContext = safeContext + dispatcher + exceptionHandler + job
196
101
return Pair (safeContext, dispatcher)
102
+ }
103
+
104
+ /* *
105
+ * This method is deprecated.
106
+ *
107
+ * @see [cleanupTestCoroutines]
108
+ */
109
+ @Deprecated(" This API has been deprecated to integrate with Structured Concurrency." ,
110
+ ReplaceWith (" scope.runBlockingTest(testBody)" , " kotlinx.coroutines.test" ),
111
+ level = DeprecationLevel .ERROR )
112
+ fun withTestContext (scope : TestCoroutineScope , testBody : suspend TestCoroutineScope .() -> Unit ) {
113
+ scope.runBlockingTest(testBody)
197
114
}
0 commit comments