Skip to content

Commit 6c54dec

Browse files
Document CoroutineTestInterceptor (#185)
Co-authored-by: Jesse Wilson <jwilson@squareup.com>
1 parent ccfbfb8 commit 6c54dec

File tree

4 files changed

+114
-3
lines changed

4 files changed

+114
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Change Log
22

33
## [Unreleased]
4-
[Unreleased]: https://github.com/cashapp/burst/compare/2.6.0...HEAD
4+
[Unreleased]: https://github.com/cashapp/burst/compare/2.7.1...HEAD
55

66
Nothing yet!
77

README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,40 @@ cleaning up
188188
intercepted DrinkSodaTest.drinkSoda
189189
```
190190

191+
### Coroutines
192+
193+
If your tests use [kotlinx-coroutines-test], you must use `CoroutineTestInterceptor` instead of
194+
`TestInterceptor`. Its intercept function suspends:
195+
196+
```kotlin
197+
class DrinkSodaTest {
198+
@InterceptTest
199+
val loggingInterceptor = object : CoroutineTestInterceptor {
200+
override suspend fun intercept(testFunction: CoroutineTestFunction) {
201+
println("intercepting $testFunction")
202+
testFunction()
203+
}
204+
}
205+
206+
@Test
207+
fun drinkSoda() = runTest {
208+
println("drinking a Pepsi")
209+
}
210+
}
211+
```
212+
213+
The `CoroutineTestFunction` has the `TestScope` property so interceptors have the same capabilities
214+
as tests.
215+
216+
You’ll also need this Gradle dependency:
217+
218+
```kotlin
219+
dependencies {
220+
testImplementation("app.cash.burst:burst-coroutines:2.7.1")
221+
...
222+
}
223+
```
224+
191225
### Features and Limitations
192226

193227
You can have multiple test interceptors in each class. They are executed in declaration order.
@@ -200,6 +234,9 @@ You can use `try/catch/finally` to execute code when tests fail.
200234
Intercepted test functions must be `final`. Mixing `@InterceptTest` with non-final test functions
201235
will cause a compilation error.
202236

237+
You cannot mix and match `CoroutinesTestInterceptor` and `TestInterceptor` in the same test. You
238+
cannot use `runTest()` with `TestInterceptor`, and you must use it with `CoroutinesTestInterceptor`.
239+
203240

204241
Gradle Setup
205242
------------
@@ -212,7 +249,7 @@ buildscript {
212249
mavenCentral()
213250
}
214251
dependencies {
215-
classpath("app.cash.burst:burst-gradle-plugin:2.6.0")
252+
classpath("app.cash.burst:burst-gradle-plugin:2.7.1")
216253
}
217254
}
218255
```
@@ -233,7 +270,7 @@ certain versions of Kotlin.
233270

234271
| Kotlin | Burst |
235272
|-----------------|---------------|
236-
| 2.2.0 | 2.6.0 |
273+
| 2.2.0 | 2.6.0 - 2.7.1 |
237274
| 2.1.20 | 2.5.0 |
238275
| 2.1.0 | 2.2.0 - 2.4.0 |
239276
| 2.0.20 - 2.0.21 | 0.1.0 - 2.1.0 |
@@ -261,3 +298,4 @@ License
261298

262299
[JUnit Rules]: https://junit.org/junit4/javadoc/4.12/org/junit/Rule.html
263300
[TestParameterInjector]: https://github.com/google/TestParameterInjector
301+
[kotlinx-coroutines-test]: https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/

burst-coroutines/src/commonMain/kotlin/app/cash/burst/coroutines/CoroutineTestInterceptor.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ package app.cash.burst.coroutines
1717

1818
import kotlinx.coroutines.test.TestScope
1919

20+
/**
21+
* Intercepts the execution of a test function, including its `@BeforeTest` and `@AfterTest`
22+
* functions.
23+
*
24+
* Test functions must make a single call to `kotlinx.coroutines.test.runTest` in their test body.
25+
*/
2026
interface CoroutineTestInterceptor {
2127
suspend fun intercept(
2228
testFunction: CoroutineTestFunction,
@@ -25,12 +31,27 @@ interface CoroutineTestInterceptor {
2531

2632
abstract class CoroutineTestFunction(
2733
val scope: TestScope,
34+
/** The package that this test is defined in, or "" it has none. */
2835
val packageName: String,
36+
/** The classes that enclose the test function, separated by '.'. */
2937
val className: String,
38+
/** The test function name. */
3039
val functionName: String,
3140
) {
41+
/**
42+
* Runs the next interceptor in the chain if there is one.
43+
*
44+
* If there isn't, it runs the following in sequence:
45+
*
46+
* * The `@BeforeTest` functions (if any)
47+
* * The `@Test` function
48+
* * The `@AfterTest` functions (if any)
49+
*/
3250
abstract suspend operator fun invoke()
3351

52+
/**
53+
* Returns the full test name, like `com.example.project.FeatureTest.testMyFeature`.
54+
*/
3455
override fun toString(): String {
3556
return buildString {
3657
if (packageName.isNotEmpty()) {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (C) 2025 Cash App
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package app.cash.burst
17+
18+
import app.cash.burst.coroutines.CoroutineTestFunction
19+
import assertk.assertThat
20+
import assertk.assertions.isEqualTo
21+
import kotlin.test.Test
22+
import kotlinx.coroutines.test.runTest
23+
24+
class CoroutineTestFunctionTest {
25+
@Test
26+
fun testToString() = runTest {
27+
val testFunction = object : CoroutineTestFunction(
28+
scope = this,
29+
packageName = "app.cash.burst.test",
30+
className = "SampleTest",
31+
functionName = "happyPath",
32+
) {
33+
override suspend fun invoke() {
34+
}
35+
}
36+
assertThat(testFunction.toString()).isEqualTo("app.cash.burst.test.SampleTest.happyPath")
37+
}
38+
39+
@Test
40+
fun testToStringWithEmptyPackageName() = runTest {
41+
val testFunction = object : CoroutineTestFunction(
42+
scope = this,
43+
packageName = "",
44+
className = "SampleTest",
45+
functionName = "happyPath",
46+
) {
47+
override suspend fun invoke() {
48+
}
49+
}
50+
assertThat(testFunction.toString()).isEqualTo("SampleTest.happyPath")
51+
}
52+
}

0 commit comments

Comments
 (0)