Skip to content

Commit bd70305

Browse files
Document @InterceptTest and test some edge cases (#173)
Co-authored-by: Jesse Wilson <jwilson@squareup.com>
1 parent 85790b8 commit bd70305

File tree

3 files changed

+213
-5
lines changed

3 files changed

+213
-5
lines changed

README.md

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
Burst
22
=====
33

4-
Burst is a library for parameterizing unit tests.
4+
Burst is a Kotlin compiler plug-in for more capable tests. It supports all Kotlin platforms and
5+
works great in multiplatform projects.
56

6-
It is similar to [TestParameterInjector] in usage, but Burst is implemented as a Kotlin compiler
7-
plug-in. Burst supports all Kotlin platforms and works great in multiplatform projects.
7+
* `@Burst` is used to parameterize unit tests. It is similar to [TestParameterInjector] in
8+
capability.
89

10+
* `@InterceptTest` is used to set up and tear down unit tests. It is similar to [JUnit Rules] in
11+
capability.
912

10-
Usage
11-
-----
13+
14+
@Burst
15+
------
1216

1317
Annotate your test class with `@Burst`.
1418

@@ -98,6 +102,105 @@ The test will be specialized for each combination of arguments.
98102
* `drinkSoda("Coke", false, Distribution.Can)`
99103
* `drinkSoda("Coke", false, Distribution.Bottle)`
100104

105+
@InterceptTest
106+
--------------
107+
108+
Implement the `TestInterceptor` interface. Your `intercept` function should call `testFunction` to
109+
run the subject test function.
110+
111+
```kotlin
112+
class RepeatInterceptor(
113+
private val attemptCount: Int,
114+
) : TestInterceptor {
115+
override fun intercept(testFunction: TestFunction) {
116+
for (i in 0 until attemptCount) {
117+
println("running ${testFunction.functionName} attempt $i")
118+
testFunction()
119+
}
120+
}
121+
}
122+
```
123+
124+
Next, declare a property for your interceptor in your test class and annotate it `@InterceptTest`:
125+
126+
```kotlin
127+
class DrinkSodaTest {
128+
@InterceptTest
129+
val repeatInterceptor = RepeatInterceptor(3)
130+
131+
@Test
132+
fun drinkSoda() {
133+
println("drinking a Pepsi")
134+
}
135+
}
136+
```
137+
138+
When you execute this test, it is intercepted:
139+
140+
```
141+
running drinkSoda attempt 0
142+
drinking a Pepsi
143+
running drinkSoda attempt 1
144+
drinking a Pepsi
145+
running drinkSoda attempt 2
146+
drinking a Pepsi
147+
```
148+
149+
### BeforeTest and AfterTest
150+
151+
If your test has these functions, the interceptor intercepts them. Here’s such a test:
152+
153+
```kotlin
154+
class DrinkSodaTest {
155+
@InterceptTest
156+
val loggingInterceptor = object : TestInterceptor {
157+
override fun intercept(testFunction: TestFunction) {
158+
println("intercepting ${testFunction.functionName}")
159+
testFunction()
160+
println("intercepted ${testFunction.functionName}")
161+
}
162+
}
163+
164+
@BeforeTest
165+
fun beforeTest() {
166+
println("getting ready")
167+
}
168+
169+
@AfterTest
170+
fun afterTest() {
171+
println("cleaning up")
172+
}
173+
174+
@Test
175+
fun drinkSoda() {
176+
println("drinking a Pepsi")
177+
}
178+
}
179+
```
180+
181+
And here’s its output:
182+
183+
```
184+
intercepting drinkSoda
185+
getting ready
186+
drinking a Pepsi
187+
cleaning up
188+
intercepted drinkSoda
189+
```
190+
191+
### Features and Limitations
192+
193+
You can have multiple test interceptors in each class. They are executed in declaration order.
194+
195+
You can use test interceptors and inheritance together. The superclass interceptors are executed
196+
first.
197+
198+
You can use `try/catch/finally` to execute code when tests fail.
199+
200+
Intercepted test functions must be `final`. Mixing `@InterceptTest` with non-final test functions
201+
will cause a compilation error.
202+
203+
101204
Gradle Setup
102205
------------
103206

@@ -156,4 +259,5 @@ License
156259
See the License for the specific language governing permissions and
157260
limitations under the License.
158261

262+
[JUnit Rules]: https://junit.org/junit4/javadoc/4.12/org/junit/Rule.html
159263
[TestParameterInjector]: https://github.com/google/TestParameterInjector

burst-kotlin-plugin-tests/src/test/kotlin/app/cash/burst/kotlin/TestInterceptorKotlinPluginTest.kt

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,6 +1304,94 @@ class TestInterceptorKotlinPluginTest {
13041304
)
13051305
}
13061306

1307+
@Test
1308+
fun interceptorGetterThrows() {
1309+
val log = BurstTester(
1310+
packageName = "app.cash.burst.tests",
1311+
).compileAndRun(
1312+
SourceFile.kotlin(
1313+
"Main.kt",
1314+
"""
1315+
package app.cash.burst.tests
1316+
1317+
import app.cash.burst.InterceptTest
1318+
import app.cash.burst.TestFunction
1319+
import app.cash.burst.TestInterceptor
1320+
import kotlin.test.AfterTest
1321+
import kotlin.test.BeforeTest
1322+
import kotlin.test.Test
1323+
1324+
class InterceptorGetterThrowsTest {
1325+
@InterceptTest
1326+
val workingInterceptor = BasicInterceptor("working")
1327+
1328+
@InterceptTest
1329+
val brokenInterceptor: TestInterceptor
1330+
get() = error("boom!")
1331+
1332+
@Test
1333+
fun test() {
1334+
log("running")
1335+
}
1336+
}
1337+
1338+
class BasicInterceptor(val name: String) : TestInterceptor {
1339+
override fun intercept(testFunction: TestFunction) {
1340+
log("intercepting ${'$'}name")
1341+
try {
1342+
testFunction()
1343+
} finally {
1344+
log("intercepted ${'$'}name")
1345+
}
1346+
}
1347+
}
1348+
1349+
fun main(vararg args: String) {
1350+
try {
1351+
InterceptorGetterThrowsTest().test()
1352+
} catch (e: Throwable) {
1353+
log("caught: ${'$'}{e.message}")
1354+
}
1355+
}
1356+
""",
1357+
),
1358+
)
1359+
1360+
assertThat(log).containsExactly(
1361+
"intercepting working",
1362+
"intercepted working",
1363+
"caught: boom!",
1364+
)
1365+
}
1366+
1367+
@Test
1368+
fun interceptorMustBeTheRightType() {
1369+
val result = compile(
1370+
SourceFile.kotlin(
1371+
"Main.kt",
1372+
"""
1373+
package com.example
1374+
1375+
import app.cash.burst.InterceptTest
1376+
import kotlin.test.Test
1377+
1378+
class InterceptorMustBeTheRightType {
1379+
@InterceptTest
1380+
val wrongType: String = "hello"
1381+
1382+
@Test
1383+
fun test() {
1384+
}
1385+
}
1386+
""",
1387+
),
1388+
)
1389+
assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, result.exitCode, result.messages)
1390+
assertThat(result.messages).contains(
1391+
"Main.kt:7:3 @InterceptTest properties must be assignable to TestInterceptor",
1392+
)
1393+
}
1394+
13071395
@Test
13081396
fun interceptedTestMustBeFinal() {
13091397
val result = compile(

burst-kotlin-plugin/src/main/kotlin/app/cash/burst/kotlin/HierarchyInterceptorInjector.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ package app.cash.burst.kotlin
1919

2020
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
2121
import org.jetbrains.kotlin.ir.declarations.IrClass
22+
import org.jetbrains.kotlin.ir.declarations.IrProperty
2223
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
2324
import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
2425
import org.jetbrains.kotlin.ir.util.functions
26+
import org.jetbrains.kotlin.ir.util.isSubtypeOfClass
2527
import org.jetbrains.kotlin.ir.util.properties
2628
import org.jetbrains.kotlin.ir.util.superClass
2729

@@ -60,6 +62,13 @@ internal class HierarchyInterceptorInjector(
6062
return null
6163
}
6264

65+
// Check the @InterceptTest property types.
66+
for (property in interceptorProperties) {
67+
if (property.getter?.returnType?.isSubtypeOfClass(burstApis.testInterceptor) != true) {
68+
unexpectedInterceptTest(property)
69+
}
70+
}
71+
6372
val originalFunctions = classDeclaration.functions.toList()
6473

6574
val interceptorInjector = InterceptorInjector(
@@ -95,4 +104,11 @@ internal class HierarchyInterceptorInjector(
95104
/** The `intercept()` function declared by this class. */
96105
private val IrClass.interceptFunction: IrSimpleFunction?
97106
get() = functions.firstOrNull { burstApis.testInterceptorIntercept in it.overriddenSymbols }
107+
108+
private fun unexpectedInterceptTest(property: IrProperty): Nothing {
109+
throw BurstCompilationException(
110+
"@InterceptTest properties must be assignable to TestInterceptor",
111+
property,
112+
)
113+
}
98114
}

0 commit comments

Comments
 (0)