Skip to content

Commit 5907981

Browse files
First step of test interceptors (#165)
* First step of test interceptors This is like JUnit 4 @rules. This is a lot of code but still incomplete. Some work still outstanding * Inheritance * JUnit 5 API testing * JUnit 4 API testing * Defensive code if there's already an intercept() function But this is already too big, so lets start here. * Spotless * apiDump, more tests * Early, optimistic, inheritance * More tests * Use declaration order * Rename TestInterceptor.Test to TestFunction * Simplify BurstKotlinPluginTest * Promote more tests to TestInterceptorKotlinPluginTest * apiDump --------- Co-authored-by: Jesse Wilson <jwilson@squareup.com>
1 parent 3b6148e commit 5907981

File tree

21 files changed

+2287
-5
lines changed

21 files changed

+2287
-5
lines changed

burst-gradle-plugin/src/test/kotlin/app/cash/burst/gradle/GradleTester.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package app.cash.burst.gradle
1818
import assertk.assertThat
1919
import assertk.assertions.isIn
2020
import java.io.File
21+
import org.gradle.testkit.runner.BuildTask
2122
import org.gradle.testkit.runner.GradleRunner
2223
import org.gradle.testkit.runner.TaskOutcome
2324

@@ -45,6 +46,11 @@ class GradleTester(
4546
}
4647
}
4748

49+
fun cleanAndBuildAndFail(taskName: String): BuildTask {
50+
val result = createRunner("clean", taskName).buildAndFail()
51+
return result.task(taskName)!!
52+
}
53+
4854
fun readTestSuite(
4955
className: String,
5056
testTaskName: String = "test",
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
17+
package app.cash.burst.gradle
18+
19+
import assertk.assertThat
20+
import assertk.assertions.contains
21+
import assertk.assertions.isEqualTo
22+
import assertk.assertions.isNotNull
23+
import assertk.assertions.isNull
24+
import org.gradle.testkit.runner.TaskOutcome
25+
import org.junit.BeforeClass
26+
import org.junit.Test
27+
28+
class TestInterceptorGradlePluginTest {
29+
companion object {
30+
private val tester = GradleTester("interceptor")
31+
32+
@BeforeClass
33+
@JvmStatic
34+
fun beforeClass() {
35+
val result = tester.cleanAndBuildAndFail(":lib:test")
36+
assertThat(result.outcome).isEqualTo(TaskOutcome.FAILED)
37+
}
38+
}
39+
40+
@Test
41+
fun happyPath() {
42+
with(tester.readTestSuite("app.cash.burst.tests.BasicTest")) {
43+
assertThat(testCases.single().failureMessage).isNull()
44+
assertThat(systemOut).isEqualTo(
45+
"""
46+
|intercepting
47+
| packageName=app.cash.burst.tests
48+
| className=BasicTest
49+
| functionName=passingTest
50+
|set up
51+
|running
52+
|tear down
53+
|intercepted
54+
|
55+
""".trimMargin(),
56+
)
57+
}
58+
}
59+
60+
@Test
61+
fun failingTest() {
62+
with(tester.readTestSuite("app.cash.burst.tests.FailingTest")) {
63+
assertThat(testCases.single().failureMessage).isNotNull().contains("boom!")
64+
assertThat(systemOut).isEqualTo(
65+
"""
66+
|set up
67+
|tear down
68+
|re-throwing exception: boom!
69+
|
70+
""".trimMargin(),
71+
)
72+
}
73+
}
74+
75+
/**
76+
* Note that this is different from JUnit 4, where rules enclose superclass' @Before functions.
77+
*/
78+
@Test
79+
fun beforeTestInSuperclass() {
80+
with(tester.readTestSuite("app.cash.burst.tests.BeforeTestInSuperclassTest${'$'}CircleTest")) {
81+
assertThat(testCases.single().failureMessage).isNull()
82+
assertThat(systemOut).isEqualTo(
83+
"""
84+
|beforeTest
85+
|intercepting
86+
|running
87+
|intercepted
88+
|
89+
""".trimMargin(),
90+
)
91+
}
92+
}
93+
94+
/**
95+
* Note that this is different from JUnit 4, where rules enclose superclass' @After functions.
96+
*/
97+
@Test
98+
fun afterTestInSuperclass() {
99+
with(tester.readTestSuite("app.cash.burst.tests.AfterTestInSuperclassTest${'$'}CircleTest")) {
100+
assertThat(testCases.single().failureMessage).isNull()
101+
assertThat(systemOut).isEqualTo(
102+
"""
103+
|intercepting
104+
|running
105+
|intercepted
106+
|afterTest
107+
|
108+
""".trimMargin(),
109+
)
110+
}
111+
}
112+
}

burst-gradle-plugin/src/test/kotlin/app/cash/burst/gradle/junitXml.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,23 @@ internal fun Element.toTestSuite(): TestSuite {
4747

4848
internal fun Element.toTestCase(): TestCase {
4949
var skipped = false
50+
var failureMessage: String? = null
5051
for (i in 0 until childNodes.length) {
5152
val item = childNodes.item(i)
52-
if (item is Element && item.tagName == "skipped") {
53-
skipped = true
53+
if (item is Element) {
54+
if (item.tagName == "skipped") {
55+
skipped = true
56+
}
57+
if (item.tagName == "failure") {
58+
failureMessage = item.getAttribute("message")
59+
}
5460
}
5561
}
5662

5763
return TestCase(
5864
name = getAttribute("name"),
5965
skipped = skipped,
66+
failureMessage = failureMessage,
6067
)
6168
}
6269

@@ -69,4 +76,5 @@ class TestSuite(
6976
class TestCase(
7077
val name: String,
7178
val skipped: Boolean,
79+
val failureMessage: String?,
7280
)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2+
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
3+
4+
buildscript {
5+
repositories {
6+
maven {
7+
url = file("$rootDir/../../../../../build/testMaven").toURI()
8+
}
9+
mavenCentral()
10+
google()
11+
}
12+
dependencies {
13+
classpath("app.cash.burst:burst-gradle-plugin:${project.property("burstVersion")}")
14+
classpath(libs.kotlin.gradlePlugin)
15+
}
16+
}
17+
18+
allprojects {
19+
repositories {
20+
maven {
21+
url = file("$rootDir/../../../../../build/testMaven").toURI()
22+
}
23+
mavenCentral()
24+
google()
25+
}
26+
27+
tasks.withType(JavaCompile::class.java).configureEach {
28+
sourceCompatibility = "1.8"
29+
targetCompatibility = "1.8"
30+
}
31+
32+
tasks.withType(KotlinJvmCompile::class.java).configureEach {
33+
compilerOptions {
34+
jvmTarget.set(JvmTarget.JVM_1_8)
35+
}
36+
}
37+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
plugins {
2+
kotlin("jvm")
3+
id("app.cash.burst")
4+
}
5+
6+
dependencies {
7+
testImplementation(kotlin("test"))
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package app.cash.burst.tests
2+
3+
import app.cash.burst.InterceptTest
4+
import app.cash.burst.TestFunction
5+
import app.cash.burst.TestInterceptor
6+
import kotlin.test.AfterTest
7+
import kotlin.test.Test
8+
9+
class AfterTestInSuperclassTest {
10+
open class ShapeTest {
11+
@AfterTest
12+
fun afterTest() {
13+
println("afterTest")
14+
}
15+
}
16+
17+
class CircleTest : ShapeTest() {
18+
@InterceptTest
19+
val interceptor = LoggingInterceptor()
20+
21+
@Test
22+
fun passingTest() {
23+
println("running")
24+
}
25+
}
26+
27+
class LoggingInterceptor : TestInterceptor {
28+
override fun intercept(testFunction: TestFunction) {
29+
println("intercepting")
30+
testFunction()
31+
println("intercepted")
32+
}
33+
}
34+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package app.cash.burst.tests
2+
3+
import app.cash.burst.InterceptTest
4+
import app.cash.burst.TestFunction
5+
import app.cash.burst.TestInterceptor
6+
import kotlin.test.AfterTest
7+
import kotlin.test.BeforeTest
8+
import kotlin.test.Test
9+
10+
class BasicTest {
11+
@InterceptTest
12+
val interceptor = BasicInterceptor()
13+
14+
@BeforeTest
15+
fun setUp() {
16+
println("set up")
17+
}
18+
19+
@AfterTest
20+
fun tearDown() {
21+
println("tear down")
22+
}
23+
24+
@Test
25+
fun passingTest() {
26+
println("running")
27+
}
28+
29+
class BasicInterceptor : TestInterceptor {
30+
override fun intercept(testFunction: TestFunction) {
31+
println("intercepting")
32+
println(" packageName=${testFunction.packageName}")
33+
println(" className=${testFunction.className}")
34+
println(" functionName=${testFunction.functionName}")
35+
testFunction()
36+
println("intercepted")
37+
}
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package app.cash.burst.tests
2+
3+
import app.cash.burst.InterceptTest
4+
import app.cash.burst.TestFunction
5+
import app.cash.burst.TestInterceptor
6+
import kotlin.test.BeforeTest
7+
import kotlin.test.Test
8+
9+
class BeforeTestInSuperclassTest {
10+
open class ShapeTest {
11+
@BeforeTest
12+
fun beforeTest() {
13+
println("beforeTest")
14+
}
15+
}
16+
17+
class CircleTest : ShapeTest() {
18+
@InterceptTest
19+
val interceptor = LoggingInterceptor()
20+
21+
@Test
22+
fun passingTest() {
23+
println("running")
24+
}
25+
}
26+
27+
class LoggingInterceptor : TestInterceptor {
28+
override fun intercept(testFunction: TestFunction) {
29+
println("intercepting")
30+
testFunction()
31+
println("intercepted")
32+
}
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package app.cash.burst.tests
2+
3+
import app.cash.burst.InterceptTest
4+
import app.cash.burst.TestFunction
5+
import app.cash.burst.TestInterceptor
6+
import kotlin.test.AfterTest
7+
import kotlin.test.BeforeTest
8+
import kotlin.test.Test
9+
10+
class FailingTest {
11+
@InterceptTest
12+
val interceptor = CatchingInterceptor()
13+
14+
@BeforeTest
15+
fun setUp() {
16+
println("set up")
17+
}
18+
19+
@AfterTest
20+
fun tearDown() {
21+
println("tear down")
22+
}
23+
24+
@Test
25+
fun failingTest() {
26+
error("boom!")
27+
}
28+
29+
class CatchingInterceptor : TestInterceptor {
30+
override fun intercept(testFunction: TestFunction) {
31+
try {
32+
testFunction()
33+
} catch (e: Throwable) {
34+
println("re-throwing exception: ${e.message}")
35+
throw e
36+
}
37+
}
38+
}
39+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
dependencyResolutionManagement {
2+
versionCatalogs {
3+
create("libs") {
4+
from(files("../../../../../gradle/libs.versions.toml"))
5+
}
6+
}
7+
}
8+
9+
include(":lib")

0 commit comments

Comments
 (0)