1
+ package kotlinx.coroutines.test
2
+
3
+ import kotlinx.coroutines.*
4
+ import java.util.concurrent.TimeoutException
5
+ import kotlin.coroutines.ContinuationInterceptor
6
+ import kotlin.coroutines.CoroutineContext
7
+ import kotlin.coroutines.coroutineContext
8
+
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
+ /* *
87
+ * Executes a [testBody] inside an immediate execution dispatcher.
88
+ *
89
+ * This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks.
90
+ * You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take
91
+ * extra time.
92
+ *
93
+ * Compared to [asyncTest], it provides a smaller API for tests that don't need detailed control over execution.
94
+ *
95
+ * ```
96
+ * @Test
97
+ * fun exampleTest() = runBlockingTest {
98
+ * val deferred = async {
99
+ * delay(1_000)
100
+ * async {
101
+ * delay(1_000)
102
+ * }.await()
103
+ * }
104
+ *
105
+ * deferred.await() // result available immediately
106
+ * }
107
+ *
108
+ * ```
109
+ *
110
+ * This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test
111
+ * conditions.
112
+ *
113
+ * In unhandled exceptions inside coroutines will not fail the test.
114
+ *
115
+ * @throws UncompletedCoroutinesError If the [testBody] does not complete (or cancel) all coroutines that it launches
116
+ * (including coroutines suspended on await).
117
+ *
118
+ * @param context An optional context, during [testBody] execution [DelayController.dispatchImmediately] will be set to true
119
+ * @param testBody The code of the unit-test.
120
+ *
121
+ * @see [asyncTest]
122
+ */
123
+ fun runBlockingTest (context : CoroutineContext ? = null, testBody : suspend TestCoroutineScope .() -> Unit ) {
124
+ val (safeContext, dispatcher) = context.checkArguments()
125
+ // smart cast dispatcher to expose interface
126
+ dispatcher as DelayController
127
+
128
+ val oldDispatch = dispatcher.dispatchImmediately
129
+ dispatcher.dispatchImmediately = true
130
+ val scope = TestCoroutineScope (safeContext)
131
+ try {
132
+
133
+ val deferred = scope.async {
134
+ scope.testBody()
135
+ }
136
+ dispatcher.advanceUntilIdle()
137
+ deferred.getCompletionExceptionOrNull()?.let {
138
+ throw it
139
+ }
140
+ scope.cleanupTestCoroutines()
141
+ val activeChildren = checkNotNull(safeContext[Job ]).children.filter { it.isActive }.toList()
142
+ if (activeChildren.isNotEmpty()) {
143
+ throw UncompletedCoroutinesError (" Test finished with active jobs: ${activeChildren} " )
144
+ }
145
+ } finally {
146
+ dispatcher.dispatchImmediately = oldDispatch
147
+ }
148
+ }
149
+
150
+ /* *
151
+ * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope].
152
+ */
153
+ fun TestCoroutineScope.runBlockingTest (block : suspend CoroutineScope .() -> Unit ) {
154
+ runBlockingTest(coroutineContext, block)
155
+ }
156
+
157
+ /* *
158
+ * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
159
+ *
160
+ */
161
+ fun TestCoroutineDispatcher.runBlockingTest (block : suspend CoroutineScope .() -> Unit ) {
162
+ runBlockingTest(this , block)
163
+ }
164
+
165
+ private fun CoroutineContext?.checkArguments (): Pair <CoroutineContext , ContinuationInterceptor > {
166
+ var safeContext= this ? : TestCoroutineExceptionHandler () + TestCoroutineDispatcher ()
167
+
168
+ val dispatcher = safeContext[ContinuationInterceptor ].run {
169
+ this ?.let {
170
+ require(this is DelayController ) { " Dispatcher must implement DelayController" }
171
+ }
172
+ this ? : TestCoroutineDispatcher ()
173
+ }
174
+
175
+ val exceptionHandler = safeContext[CoroutineExceptionHandler ].run {
176
+ this ?.let {
177
+ require(this is ExceptionCaptor ) { " coroutineExceptionHandler must implement ExceptionCaptor" }
178
+ }
179
+ this ? : TestCoroutineExceptionHandler ()
180
+ }
181
+
182
+ val job = safeContext[Job ] ? : SupervisorJob ()
183
+
184
+ safeContext = safeContext + dispatcher + exceptionHandler + job
185
+ return Pair (safeContext, dispatcher)
186
+ }
0 commit comments