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
+
8
+ /* *
9
+ * Executes a [testBody] in a [TestCoroutineScope] which provides detailed control over the execution of coroutines.
10
+ *
11
+ * This function should be used when you need detailed control over the execution of your test. For most tests consider
12
+ * using [runBlockingTest].
13
+ *
14
+ * Code executed in a `asyncTest` will dispatch lazily. That means calling builders such as [launch] or [async] will
15
+ * not execute the block immediately. You can use methods like [TestCoroutineScope.runCurrent] and
16
+ * [TestCoroutineScope.advanceTimeTo] on the [TestCoroutineScope]. For a full list of execution methods see
17
+ * [DelayController].
18
+ *
19
+ * ```
20
+ * @Test
21
+ * fun exampleTest() = asyncTest {
22
+ * // 1: launch will execute but not run the body
23
+ * launch {
24
+ * // 3: the body of launch will execute in response to runCurrent [currentTime = 0ms]
25
+ * delay(1_000)
26
+ * // 5: After the time is advanced, delay(1_000) will return [currentTime = 1000ms]
27
+ * println("Faster delays!")
28
+ * }
29
+ *
30
+ * // 2: use runCurrent() to execute the body of launch [currentTime = 0ms]
31
+ * runCurrent()
32
+ *
33
+ * // 4: advance the dispatcher "time" by 1_000, which will resume after the delay
34
+ * advanceTimeTo(1_000)
35
+ *
36
+ * ```
37
+ *
38
+ * This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test
39
+ * conditions.
40
+ *
41
+ * In addition any unhandled exceptions thrown in coroutines must be rethrown by
42
+ * [TestCoroutineScope.rethrowUncaughtCoroutineException] or cleared via [TestCoroutineScope.exceptions] inside of
43
+ * [testBody].
44
+ *
45
+ * @throws UncompletedCoroutinesError If the [testBody] does not complete (or cancel) all coroutines that it launches
46
+ * (including coroutines suspended on await).
47
+ * @throws UnhandledExceptionsError If an uncaught exception is not handled by [testBody]
48
+ *
49
+ * @param dispatcher An optional dispatcher, during [testBody] execution [TestCoroutineDispatcher.dispatchImmediately] will be set to false
50
+ * @param testBody The code of the unit-test.
51
+ *
52
+ * @see [runBlockingTest]
53
+ */
54
+ fun asyncTest (context : CoroutineContext ? = null, testBody : TestCoroutineScope .() -> Unit ) {
55
+ val (safeContext, dispatcher) = context.checkArguments()
56
+ // smart cast dispatcher to expose interface
57
+ dispatcher as DelayController
58
+ val scope = TestCoroutineScope (safeContext)
59
+
60
+ val oldDispatch = dispatcher.dispatchImmediately
61
+ dispatcher.dispatchImmediately = false
62
+
63
+ try {
64
+ scope.testBody()
65
+ scope.cleanupTestCoroutines()
66
+
67
+ // check for any active child jobs after cleanup (e.g. coroutines suspended on calls to await)
68
+ val job = checkNotNull(safeContext[Job ]) { " Job required for asyncTest" }
69
+ val activeChildren = job.children.filter { it.isActive }.toList()
70
+ if (activeChildren.isNotEmpty()) {
71
+ throw UncompletedCoroutinesError (" Test finished with active jobs: ${activeChildren} " )
72
+ }
73
+ } finally {
74
+ dispatcher.dispatchImmediately = oldDispatch
75
+ }
76
+ }
77
+
78
+ /* *
79
+ * @see [asyncTest]
80
+ */
81
+ fun TestCoroutineScope.asyncTest (testBody : TestCoroutineScope .() -> Unit ) =
82
+ asyncTest(coroutineContext, testBody)
83
+
84
+ /* *
85
+ * @see [asyncTest]
86
+ */
87
+ fun TestCoroutineDispatcher.asyncTest (testBody : TestCoroutineScope .() -> Unit ) =
88
+ asyncTest(this , testBody)
89
+
90
+ /* *
91
+ * Executes a [testBody] inside an immediate execution dispatcher.
92
+ *
93
+ * This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks.
94
+ * You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take
95
+ * extra time.
96
+ *
97
+ * Compared to [asyncTest], it provides a smaller API for tests that don't need detailed control over execution.
98
+ *
99
+ * ```
100
+ * @Test
101
+ * fun exampleTest() = runBlockingTest {
102
+ * val deferred = async {
103
+ * delay(1_000)
104
+ * async {
105
+ * delay(1_000)
106
+ * }.await()
107
+ * }
108
+ *
109
+ * deferred.await() // result available immediately
110
+ * }
111
+ *
112
+ * ```
113
+ *
114
+ * [runBlockingTest] will allow tests to finish successfully while started coroutines are unfinished. In addition unhandled
115
+ * exceptions inside coroutines will not fail the test.
116
+ *
117
+ * @param dispatcher An optional dispatcher, during [testBody] execution [TestCoroutineDispatcher.dispatchImmediately] will be set to true
118
+ * @param testBody The code of the unit-test.
119
+ *
120
+ * @see [asyncTest]
121
+ */
122
+ fun runBlockingTest (context : CoroutineContext ? = null, testBody : suspend CoroutineScope .() -> Unit ) {
123
+ val (safeContext, dispatcher) = context.checkArguments()
124
+ // smart cast dispatcher to expose interface
125
+ dispatcher as DelayController
126
+
127
+ val oldDispatch = dispatcher.dispatchImmediately
128
+ dispatcher.dispatchImmediately = true
129
+ val scope = TestCoroutineScope (safeContext)
130
+ try {
131
+ runBlocking(scope.coroutineContext) {
132
+ scope.testBody()
133
+ scope.cleanupTestCoroutines()
134
+ }
135
+ } finally {
136
+ dispatcher.dispatchImmediately = oldDispatch
137
+ }
138
+ }
139
+
140
+ /* *
141
+ * Convenience method for calling runBlocking on an existing [TestCoroutineScope].
142
+ *
143
+ * [block] will be executed in immediate execution mode, similar to [runBlockingTest].
144
+ */
145
+ fun <T > TestCoroutineScope.runBlocking (block : suspend CoroutineScope .() -> T ): T {
146
+ val oldDispatch = dispatchImmediately
147
+ dispatchImmediately = true
148
+ try {
149
+ return runBlocking(coroutineContext, block)
150
+ } finally {
151
+ dispatchImmediately = oldDispatch
152
+ }
153
+ }
154
+
155
+ /* *
156
+ * Convenience method for calling runBlocking on an existing [TestCoroutineDispatcher].
157
+ *
158
+ * [block] will be executed in immediate execution mode, similar to [runBlockingTest].
159
+ */
160
+ fun <T > TestCoroutineDispatcher.runBlocking (block : suspend CoroutineScope .() -> T ): T {
161
+ val oldDispatch = dispatchImmediately
162
+ dispatchImmediately = true
163
+ try {
164
+ return runBlocking(this , block)
165
+ } finally {
166
+ dispatchImmediately = oldDispatch
167
+ }
168
+ }
169
+
170
+ private fun CoroutineContext?.checkArguments (): Pair <CoroutineContext , ContinuationInterceptor > {
171
+ var safeContext= this ? : TestCoroutineExceptionHandler () + TestCoroutineDispatcher ()
172
+
173
+ val dispatcher = safeContext[ContinuationInterceptor ].run {
174
+ this ?.let {
175
+ if (this !is DelayController ) {
176
+ throw IllegalArgumentException (" Dispatcher must implement DelayController" )
177
+ }
178
+ }
179
+ this ? : TestCoroutineDispatcher ()
180
+ }
181
+
182
+ val exceptionHandler = safeContext[CoroutineExceptionHandler ].run {
183
+ this ?.let {
184
+ if (this !is ExceptionCaptor ) {
185
+ throw IllegalArgumentException (" coroutineExceptionHandler must implement ExceptionCaptor" )
186
+ }
187
+ }
188
+ this ? : TestCoroutineExceptionHandler ()
189
+ }
190
+
191
+ val job = safeContext[Job ] ? : SupervisorJob ()
192
+
193
+ safeContext = safeContext + dispatcher + exceptionHandler + job
194
+ return Pair (safeContext, dispatcher)
195
+ }
0 commit comments