5
5
package kotlinx.coroutines.test
6
6
7
7
import kotlinx.coroutines.*
8
+ import kotlinx.coroutines.selects.*
8
9
import kotlin.coroutines.*
9
10
10
11
/* *
@@ -42,9 +43,10 @@ import kotlin.coroutines.*
42
43
* @param testBody The code of the unit-test.
43
44
*/
44
45
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
46
+ @Deprecated(" Use `runTest` instead to support completing from other dispatchers." , level = DeprecationLevel .WARNING )
45
47
public fun runBlockingTest (context : CoroutineContext = EmptyCoroutineContext , testBody : suspend TestCoroutineScope .() -> Unit ) {
46
48
val scope = TestCoroutineScope (context)
47
- val scheduler = scope.coroutineContext[ TestCoroutineScheduler ] !!
49
+ val scheduler = scope.testScheduler
48
50
val deferred = scope.async {
49
51
scope.testBody()
50
52
}
@@ -69,3 +71,139 @@ public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.
69
71
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
70
72
public fun TestCoroutineDispatcher.runBlockingTest (block : suspend TestCoroutineScope .() -> Unit ): Unit =
71
73
runBlockingTest(this , block)
74
+
75
+ /* *
76
+ * A test result.
77
+ *
78
+ * * On JVM and Native, this resolves to [Unit], representing the fact that tests are run in a blocking manner on these
79
+ * platforms: a call to a function returning a [TestResult] will simply execute the test inside it.
80
+ * * On JS, this is a `Promise`, which reflects the fact that the test-running function does not wait for a test to
81
+ * finish. The JS test frameworks typically support returning `Promise` from a test and will correctly handle it.
82
+ *
83
+ * Because of the behavior on JS, extra care must be taken when writing multiplatform tests to avoid losing test errors:
84
+ * * Don't do anything after running the functions returning a [TestResult]. On JS, this code will execute *before* the
85
+ * test finishes.
86
+ * * As a corollary, don't run functions returning a [TestResult] more than once per test. The only valid thing to do
87
+ * with a [TestResult] is to immediately `return` it from a test.
88
+ * * Don't nest functions returning a [TestResult].
89
+ */
90
+ @Suppress(" NO_ACTUAL_FOR_EXPECT" )
91
+ public expect class TestResult
92
+
93
+ /* *
94
+ * Executes [testBody] as a test, returning [TestResult].
95
+ *
96
+ * On JVM and Native, this function behaves similarly to [runBlocking], with the difference that the code that it runs
97
+ * will skip delays. This allows to use [delay] in without causing the tests to take more time than necessary.
98
+ * On JS, this function creates a `Promise` that executes the test body with the delay-skipping behavior.
99
+ *
100
+ * ```
101
+ * @Test
102
+ * fun exampleTest() = runTest {
103
+ * val deferred = async {
104
+ * delay(1_000)
105
+ * async {
106
+ * delay(1_000)
107
+ * }.await()
108
+ * }
109
+ *
110
+ * deferred.await() // result available immediately
111
+ * }
112
+ * ```
113
+ *
114
+ * The platform difference entails that, in order to use this function correctly in common code, one must always
115
+ * immediately return the produced [TestResult] from the test method, without doing anything else afterwards. See
116
+ * [TestResult] for details on this.
117
+ *
118
+ * ### Delay-skipping
119
+ *
120
+ * Delay-skipping is achieved by using virtual time. [TestCoroutineScheduler] is automatically created (if it wasn't
121
+ * passed in some way in [context]) and can be used to control the virtual time, advancing it, running the tasks
122
+ * scheduled at a specific time etc. Some convenience methods are available on [TestCoroutineScope] to control the
123
+ * scheduler.
124
+ *
125
+ * Delays in code that runs inside dispatchers that don't use a [TestCoroutineScheduler] don't get skipped:
126
+ * ```
127
+ * @Test
128
+ * fun exampleTest() = runTest {
129
+ * val elapsed = TimeSource.Monotonic.measureTime {
130
+ * val deferred = async {
131
+ * delay(1_000) // will be skipped
132
+ * withContext(Dispatchers.Default) {
133
+ * delay(5_000) // Dispatchers.Default don't know about TestCoroutineScheduler
134
+ * }
135
+ * }
136
+ * deferred.await()
137
+ * }
138
+ * println(elapsed) // about five seconds
139
+ * }
140
+ * ```
141
+ *
142
+ * ### Failures
143
+ *
144
+ * This method requires that all coroutines launched inside [testBody] complete, or are cancelled. Otherwise, the test
145
+ * will be failed (which, on JVM and Native, means that [runTest] itself will throw [UncompletedCoroutinesError],
146
+ * whereas on JS, the `Promise` will fail with it).
147
+ *
148
+ * In the general case, if there are active jobs, it's impossible to detect if they are going to complete eventually due
149
+ * to the asynchronous nature of coroutines. In order to prevent tests hanging in this scenario, [runTest] will wait
150
+ * for [dispatchTimeoutMs] milliseconds (by default, 10 seconds) from the moment when [TestCoroutineScheduler] becomes
151
+ * idle before throwing [UncompletedCoroutinesError]. If some dispatcher linked to [TestCoroutineScheduler] receives a
152
+ * task during that time, the timer gets reset.
153
+ *
154
+ * Unhandled exceptions thrown by coroutines in the test will be rethrown at the end of the test.
155
+ *
156
+ * ### Configuration
157
+ *
158
+ * [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine
159
+ * scope created for the test, [context] also can be used to change how the test is executed.
160
+ * See the [TestCoroutineScope] constructor documentation for details.
161
+ *
162
+ * @throws IllegalArgumentException if the [context] is invalid. See the [TestCoroutineScope] constructor docs for
163
+ * details.
164
+ */
165
+ public fun runTest (
166
+ context : CoroutineContext = EmptyCoroutineContext ,
167
+ dispatchTimeoutMs : Long = 10_000,
168
+ testBody : suspend TestCoroutineScope .() -> Unit
169
+ ): TestResult = createTestResult {
170
+ val testScope = TestCoroutineScope (context)
171
+ val scheduler = testScope.testScheduler
172
+ val deferred = testScope.async {
173
+ testScope.testBody()
174
+ }
175
+ var completed = false
176
+ while (! completed) {
177
+ scheduler.advanceUntilIdle()
178
+ if (deferred.isCompleted) {
179
+ /* don't even enter `withTimeout`; this allows to use a timeout of zero to check that there are no
180
+ non-trivial dispatches. */
181
+ completed = true
182
+ continue
183
+ }
184
+ try {
185
+ withTimeout(dispatchTimeoutMs) {
186
+ select<Unit > {
187
+ deferred.onAwait {
188
+ completed = true
189
+ }
190
+ scheduler.onDispatchEvent {
191
+ // we received knowledge that `scheduler` observed a dispatch event, so we reset the timeout
192
+ }
193
+ }
194
+ }
195
+ } catch (e: TimeoutCancellationException ) {
196
+ throw UncompletedCoroutinesError (" The test coroutine was not completed after waiting for $dispatchTimeoutMs ms" )
197
+ }
198
+ }
199
+ deferred.getCompletionExceptionOrNull()?.let {
200
+ throw it
201
+ }
202
+ testScope.cleanupTestCoroutines()
203
+ }
204
+
205
+ /* *
206
+ * Runs [testProcedure], creating a [TestResult].
207
+ */
208
+ @Suppress(" NO_ACTUAL_FOR_EXPECT" ) // actually suppresses `TestResult`
209
+ internal expect fun createTestResult (testProcedure : suspend () -> Unit ): TestResult
0 commit comments