Skip to content

Commit d5bd531

Browse files
수현claude
andcommitted
test: migrate two suites to TestBalloon as a PoC
Wire the TestBalloon plugin into jindong-core and jindong-compose alongside Kotest, and migrate exactly two suites to it: - HapticIntensityTest: data-driven cases become one test per input via a plain Kotlin loop, so each input is named and fails independently (7 tests -> 14, same assertions). - JindongApplierTest: the lateinit + beforeEach fixture is replaced by testFixture { Fixture() } asContextForEach, making isolation structural. Test bodies are unchanged apart from context -> testSuite. Assertions stay on kotest-assertions-core in both; only the spec engine changed. Every other suite is left on Kotest, which verifies the two frameworks coexist in the same source set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent f3e9a27 commit d5bd531

5 files changed

Lines changed: 72 additions & 60 deletions

File tree

gradle/libs.versions.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ composeMultiplatform = "1.10.0"
1111
composeBom = "2025.12.01"
1212
coroutines = "1.10.2"
1313
kotest = "6.0.7"
14+
testballoon = "1.0.1-K2.3.0"
1415
ksp = "2.3.4"
1516
robolectric = "4.16"
1617
androidxTest = "1.7.0"
@@ -29,6 +30,7 @@ kotest-framework-engine = { module = "io.kotest:kotest-framework-engine", versio
2930
kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
3031
kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" }
3132
kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
33+
testballoon-framework-core = { module = "de.infix.testBalloon:testBalloon-framework-core", version.ref = "testballoon" }
3234
junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" }
3335
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
3436
androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTest" }
@@ -46,6 +48,7 @@ vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "
4648
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
4749
binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" }
4850
kotest = { id = "io.kotest", version.ref = "kotest" }
51+
testballoon = { id = "de.infix.testBalloon", version.ref = "testballoon" }
4952
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
5053
android-application = { id = "com.android.application", version.ref = "agp" }
5154
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }

jindong-compose/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ plugins {
2121
alias(libs.plugins.compose.compiler)
2222
alias(libs.plugins.vanniktech.mavenPublish)
2323
alias(libs.plugins.kotest)
24+
alias(libs.plugins.testballoon)
2425
alias(libs.plugins.ksp)
2526
alias(libs.plugins.binaryCompatibilityValidator)
2627
}
@@ -71,6 +72,8 @@ kotlin {
7172
implementation(libs.kotest.assertions.core)
7273
implementation(libs.compose.ui.test)
7374
implementation(libs.kotlinx.coroutines.test)
75+
// PoC: TestBalloon running alongside Kotest in the same source set.
76+
implementation(libs.testballoon.framework.core)
7477
}
7578
}
7679
}

jindong-compose/src/commonTest/kotlin/io/github/compose/jindong/compose/JindongApplierTest.kt

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,40 +18,38 @@
1818
package io.github.compose.jindong.compose
1919

2020
import androidx.compose.runtime.ExperimentalComposeApi
21+
import de.infix.testBalloon.framework.core.testSuite
2122
import io.github.compose.jindong.core.element.HapticElement
2223
import io.github.compose.jindong.core.element.SequenceElement
2324
import io.github.compose.jindong.core.element.VibrationElement
2425
import io.github.compose.jindong.core.model.HapticIntensity
2526
import io.kotest.assertions.throwables.shouldThrow
26-
import io.kotest.core.spec.style.FunSpec
2727
import io.kotest.matchers.collections.shouldBeEmpty
2828
import io.kotest.matchers.collections.shouldContainExactly
2929
import io.kotest.matchers.shouldBe
3030

3131
/**
32-
* Tests for [JindongApplier] using Kotest FunSpec.
32+
* PoC migration of the former Kotest `JindongApplierTest` (FunSpec) to TestBalloon.
3333
*
34-
* Verifies correct tree manipulation operations:
35-
* - Insert (top-down and bottom-up)
36-
* - Remove (single and multiple elements)
37-
* - Move (forward, backward, multiple elements)
38-
* - Clear
39-
* - Navigation (down/up)
34+
* The `lateinit var root/applier` + `beforeEach` pattern is replaced by a single `testFixture`. Bound
35+
* with `asContextForEach`, every test — including those in nested suites — receives a freshly built
36+
* [Fixture] as its receiver, so `root`/`applier` read exactly as they did before while the isolation
37+
* is now structural rather than a re-init hook that has to run first. Assertions stay on
38+
* kotest-assertions-core, so only the spec engine changed.
39+
*
40+
* Covers the same tree operations: insert (top-down), remove, move, clear, navigation, edge cases.
4041
*/
41-
class JindongApplierTest :
42-
FunSpec({
43-
44-
fun createElement(durationMs: Long): VibrationElement = VibrationElement(durationMs = durationMs, intensity = HapticIntensity.HIGH)
42+
val JindongApplierTest by testSuite {
4543

46-
lateinit var root: HapticElement
47-
lateinit var applier: JindongApplier
44+
fun createElement(durationMs: Long): VibrationElement = VibrationElement(durationMs = durationMs, intensity = HapticIntensity.HIGH)
4845

49-
beforeEach {
50-
root = SequenceElement()
51-
applier = JindongApplier(root)
52-
}
46+
class Fixture {
47+
val root: HapticElement = SequenceElement()
48+
val applier = JindongApplier(root)
49+
}
5350

54-
context("JindongApplier insertTopDown") {
51+
testFixture { Fixture() } asContextForEach {
52+
testSuite("JindongApplier insertTopDown") {
5553
test("should add element at index 0") {
5654
val element = createElement(100)
5755

@@ -93,7 +91,7 @@ class JindongApplierTest :
9391
}
9492
}
9593

96-
context("JindongApplier remove") {
94+
testSuite("JindongApplier remove") {
9795
test("should remove single element at index") {
9896
val element1 = createElement(100)
9997
val element2 = createElement(200)
@@ -142,8 +140,8 @@ class JindongApplierTest :
142140
}
143141
}
144142

145-
context("JindongApplier move") {
146-
context("moving single element") {
143+
testSuite("JindongApplier move") {
144+
testSuite("moving single element") {
147145
test("should move element forward") {
148146
val element1 = createElement(100)
149147
val element2 = createElement(200)
@@ -199,7 +197,7 @@ class JindongApplierTest :
199197
}
200198
}
201199

202-
context("moving multiple elements") {
200+
testSuite("moving multiple elements") {
203201
test("should move multiple elements forward") {
204202
val elements = (1..5).map { createElement(it * 100L) }
205203
elements.forEach { applier.insertTopDown(root.children.size, it) }
@@ -240,7 +238,7 @@ class JindongApplierTest :
240238
}
241239
}
242240

243-
context("complex move scenarios") {
241+
testSuite("complex move scenarios") {
244242
test("should handle sequential move operations correctly") {
245243
val elements = (1..7).map { createElement(it * 100L) }
246244
elements.forEach { applier.insertTopDown(root.children.size, it) }
@@ -263,7 +261,7 @@ class JindongApplierTest :
263261
}
264262
}
265263

266-
context("JindongApplier clear") {
264+
testSuite("JindongApplier clear") {
267265
test("should remove all children from root") {
268266
val element1 = createElement(100)
269267
val element2 = createElement(200)
@@ -289,7 +287,7 @@ class JindongApplierTest :
289287
}
290288
}
291289

292-
context("JindongApplier navigation") {
290+
testSuite("JindongApplier navigation") {
293291
test("should navigate down and up correctly") {
294292
val containerElement = SequenceElement()
295293
val leafElement = createElement(100)
@@ -330,7 +328,7 @@ class JindongApplierTest :
330328
}
331329
}
332330

333-
context("JindongApplier edge cases") {
331+
testSuite("JindongApplier edge cases") {
334332
test("should handle empty operations on empty root") {
335333
root.children.shouldBeEmpty()
336334

@@ -364,4 +362,5 @@ class JindongApplierTest :
364362
root.children.shouldBeEmpty()
365363
}
366364
}
367-
})
365+
}
366+
}

jindong-core/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ plugins {
2020
alias(libs.plugins.android.kotlin.multiplatform.library)
2121
alias(libs.plugins.vanniktech.mavenPublish)
2222
alias(libs.plugins.kotest)
23+
alias(libs.plugins.testballoon)
2324
alias(libs.plugins.ksp)
2425
alias(libs.plugins.binaryCompatibilityValidator)
2526
alias(libs.plugins.kover)
@@ -70,6 +71,8 @@ kotlin {
7071
implementation(libs.kotest.framework.engine)
7172
implementation(libs.kotest.assertions.core)
7273
implementation(libs.kotest.property)
74+
// PoC: TestBalloon running alongside Kotest in the same source set.
75+
implementation(libs.testballoon.framework.core)
7376
}
7477

7578
named("androidHostTest").dependencies {

jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/HapticIntensityTest.kt

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,44 +15,48 @@
1515
*/
1616
package io.github.compose.jindong.core.model
1717

18-
import io.kotest.core.spec.style.FunSpec
18+
import de.infix.testBalloon.framework.core.testSuite
1919
import io.kotest.matchers.shouldBe
2020

21-
class HapticIntensityTest :
22-
FunSpec({
23-
context("Pre-defined levels") {
24-
test("LIGHT should have value 0.25") {
25-
HapticIntensity.LIGHT.value shouldBe 0.25f
26-
}
27-
test("MEDIUM should have value 0.5") {
28-
HapticIntensity.MEDIUM.value shouldBe 0.5f
29-
}
30-
test("STRONG should have value 0.75") {
31-
HapticIntensity.STRONG.value shouldBe 0.75f
32-
}
33-
test("HIGH should have value 1.0") {
34-
HapticIntensity.HIGH.value shouldBe 1.0f
21+
/**
22+
* PoC migration of the former Kotest `HapticIntensityTest` (FunSpec) to TestBalloon.
23+
*
24+
* The data-driven cases previously sat as several `shouldBe` calls inside one test body, where the
25+
* first failing input aborts the rest and the test name says nothing about which input broke. Each
26+
* input is now its own test: a plain Kotlin `for` loop over `test(...)` is all TestBalloon needs for
27+
* parameterization. Assertions stay on kotest-assertions-core (`shouldBe`), so only the spec engine
28+
* changed.
29+
*/
30+
val HapticIntensityTest by testSuite {
31+
testSuite("Pre-defined levels") {
32+
val levels = listOf(
33+
Triple("LIGHT", HapticIntensity.LIGHT, 0.25f),
34+
Triple("MEDIUM", HapticIntensity.MEDIUM, 0.5f),
35+
Triple("STRONG", HapticIntensity.STRONG, 0.75f),
36+
Triple("HIGH", HapticIntensity.HIGH, 1.0f),
37+
)
38+
for ((name, level, expected) in levels) {
39+
test("$name should have value $expected") {
40+
level.value shouldBe expected
3541
}
3642
}
43+
}
3744

38-
context("Custom intensity") {
39-
test("should coerce values above 1.0 to 1.0") {
40-
HapticIntensity.Custom(1.5f).value shouldBe 1.0f
41-
HapticIntensity.Custom(2.0f).value shouldBe 1.0f
42-
HapticIntensity.Custom(100f).value shouldBe 1.0f
45+
testSuite("Custom intensity") {
46+
for (input in listOf(1.5f, 2.0f, 100f)) {
47+
test("should coerce $input above 1.0 to 1.0") {
48+
HapticIntensity.Custom(input).value shouldBe 1.0f
4349
}
44-
45-
test("should coerce values below 0.0 to 0.0") {
46-
HapticIntensity.Custom(-0.1f).value shouldBe 0.0f
47-
HapticIntensity.Custom(-1.0f).value shouldBe 0.0f
48-
HapticIntensity.Custom(-100f).value shouldBe 0.0f
50+
}
51+
for (input in listOf(-0.1f, -1.0f, -100f)) {
52+
test("should coerce $input below 0.0 to 0.0") {
53+
HapticIntensity.Custom(input).value shouldBe 0.0f
4954
}
50-
51-
test("should preserve valid values") {
52-
HapticIntensity.Custom(0.0f).value shouldBe 0.0f
53-
HapticIntensity.Custom(0.5f).value shouldBe 0.5f
54-
HapticIntensity.Custom(1.0f).value shouldBe 1.0f
55-
HapticIntensity.Custom(0.33f).value shouldBe 0.33f
55+
}
56+
for (input in listOf(0.0f, 0.5f, 1.0f, 0.33f)) {
57+
test("should preserve valid value $input") {
58+
HapticIntensity.Custom(input).value shouldBe input
5659
}
5760
}
58-
})
61+
}
62+
}

0 commit comments

Comments
 (0)