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
+ /* *
86
+ * Executes a [testBody] inside an immediate execution dispatcher.
87
+ *
88
+ * This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks.
89
+ * You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take
90
+ * extra time.
91
+ *
92
+ * Compared to [asyncTest], it provides a smaller API for tests that don't need detailed control over execution.
93
+ *
94
+ * ```
95
+ * @Test
96
+ * fun exampleTest() = runBlockingTest {
97
+ * val deferred = async {
98
+ * delay(1_000)
99
+ * async {
100
+ * delay(1_000)
101
+ * }.await()
102
+ * }
103
+ *
104
+ * deferred.await() // result available immediately
105
+ * }
106
+ *
107
+ * ```
108
+ *
109
+ * [runBlockingTest] will allow tests to finish successfully while started coroutines are unfinished. In addition unhandled
110
+ * exceptions inside coroutines will not fail the test.
111
+ *
112
+ * @param dispatcher An optional dispatcher, during [testBody] execution [TestCoroutineDispatcher.dispatchImmediately] will be set to true
113
+ * @param testBody The code of the unit-test.
114
+ *
115
+ * @see [asyncTest]
116
+ */
117
+ fun runBlockingTest (context : CoroutineContext ? = null, testBody : suspend CoroutineScope .() -> Unit ) {
118
+ val (safeContext, dispatcher) = context.checkArguments()
119
+ // smart cast dispatcher to expose interface
120
+ dispatcher as DelayController
121
+
122
+ val oldDispatch = dispatcher.dispatchImmediately
123
+ dispatcher.dispatchImmediately = true
124
+ val scope = TestCoroutineScope (safeContext)
125
+ try {
126
+ runBlocking(scope.coroutineContext) {
127
+ scope.testBody()
128
+ scope.cleanupTestCoroutines()
129
+ }
130
+ } finally {
131
+ dispatcher.dispatchImmediately = oldDispatch
132
+ }
133
+ }
134
+
135
+ /* *
136
+ * Convenience method for calling runBlocking on an existing [TestCoroutineScope].
137
+ *
138
+ * [block] will be executed in immediate execution mode, similar to [runBlockingTest].
139
+ */
140
+ fun <T > TestCoroutineScope.runBlocking (block : suspend CoroutineScope .() -> T ): T {
141
+ val oldDispatch = dispatchImmediately
142
+ dispatchImmediately = true
143
+ try {
144
+ return runBlocking(coroutineContext, block)
145
+ } finally {
146
+ dispatchImmediately = oldDispatch
147
+ }
148
+ }
149
+
150
+ /* *
151
+ * Convenience method for calling runBlocking on an existing [TestCoroutineDispatcher].
152
+ *
153
+ * [block] will be executed in immediate execution mode, similar to [runBlockingTest].
154
+ */
155
+ fun <T > TestCoroutineDispatcher.runBlocking (block : suspend CoroutineScope .() -> T ): T {
156
+ val oldDispatch = dispatchImmediately
157
+ dispatchImmediately = true
158
+ try {
159
+ return runBlocking(this , block)
160
+ } finally {
161
+ dispatchImmediately = oldDispatch
162
+ }
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
+ if (this !is DelayController ) {
171
+ throw IllegalArgumentException (" Dispatcher must implement DelayController" )
172
+ }
173
+ }
174
+ this ? : TestCoroutineDispatcher ()
175
+ }
176
+
177
+ val exceptionHandler = safeContext[CoroutineExceptionHandler ].run {
178
+ this ?.let {
179
+ if (this !is ExceptionCaptor ) {
180
+ throw IllegalArgumentException (" coroutineExceptionHandler must implement ExceptionCaptor" )
181
+ }
182
+ }
183
+ this ? : TestCoroutineExceptionHandler ()
184
+ }
185
+
186
+ val job = safeContext[Job ] ? : SupervisorJob ()
187
+
188
+ safeContext = safeContext + dispatcher + exceptionHandler + job
189
+ return Pair (safeContext, dispatcher)
190
+ }
0 commit comments