testBalloon
:icons: font
[.float-group] == image:documentation/images/Logo.svg[TestBalloon logo,120,120] TestBalloon
=== Structured testing for Kotlin Multiplatform
[quote] TestBalloon is a coroutines-powered test framework. It is lightweight, heavy-lifting, and easy to use (like a balloon).
image::documentation/images/Test%20Run.png[Example Test Run]
=== How to use TestBalloon
. Add the TestBalloon Gradle plugin to your build script: +
plugins { id("de.infix.testBalloon") version "VERSION"
}
. Add a dependency for the TestBalloon framework core library: +
[source,kotlin]
commonTest { dependencies { implementation("de.infix.testBalloon:testBalloon-framework-core:VERSION") }
}
. Add a dependency for the assertion library of your choice. ** For kotlin-test assertions: +
[source,kotlin]
implementation(kotlin("test"))
** For Kotest assertions with support for soft assertion and clues: +
[source,kotlin]
implementation("de.infix.testBalloon:testBalloon-integration-kotest-assertions:VERSION")
. Write a test: +
[source,kotlin]
val MyFirstTestSuite by testSuite { test("string length") { assertEquals(8, "Test me!".length) }
}
. Run tests via the familiar Gradle test targets.
. Coming soon: Install the TestBalloon plugin for IntelliJ IDEA from the marketplace to run individual tests or test suites via the editor's gutter icons.
=== What to expect
==== Structured testing, unified multiplatform API
The TestBalloon DSL uses two central functions, testSuite
and test
. Test suites can nest on all platforms, including Kotlin/JS and Kotlin/Wasm.
TestBalloon has a unified API for all targets, residing in the common
source set.
[source,kotlin]
val MyTestSuite by testSuite { <1> test("string length") { <2> assertEquals(8, "Test me!".length) <3> }
testSuite("integer operations") { <4>
test("max") {
assertEquals(5, max(5, 3))
}
test("min") {
delay(10.milliseconds) <5>
assertEquals(3, min(5, 3))
}
}
}
<1> Define a top-level test suite.
<2> Define a test.
<3> Use the assertion library of your choice.
<4> Nest test suites.
<5> Use coroutines everywhere, in a TestScope
by default.
==== Coroutines everywhere
When tests execute, each test suite becomes a coroutine, as does each test. These coroutines nest naturally, making it easy to inherit coroutine contexts and manage resource setup and tear-down (more on this later).
==== Dynamic tests in plain Kotlin
Inside the trailing lambdas of testSuite
and test
, you can use all Kotlin constructs (variable scopes, conditions, loops) to create tests dynamically. There is no extra dynamic/data/factory API you need to learn. Also, no annotations, no magic.
[source,kotlin]
val Dynamic by testSuite { val testCases = mapOf( "one" to 3, "two" to 3, "three" to 5 )
testCases.forEach { (string, expectedLength) ->
test("length of '$string'") {
assertEquals(expectedLength, string.length)
}
}
}
==== Test fixtures
You can use test fixtures for efficient, scoped shared state:
[source,kotlin]
val MyTestSuite by testSuite { val starRepository = testFixture { <1> StarRepository() <2> } closeWith { disconnect() <3> }
testSuite("actual users") {
test("alina") {
assertEquals(4, starRepository().userStars("alina")) <4>
}
test("peter") {
assertEquals(3, starRepository().userStars("peter")) <5>
}
}
} <6>
<1> Declare a test fixture at zero cost if not used.
<2> Use suspend functions in setup code.
<3> Use suspend functions in (optional) tear-down code.
<4> Use the fixture, which initializes lazily.
<5> Reuse the same fixture in other tests, sharing its setup cost.
<6> The fixture will close automatically when its suite finishes.
==== Extensible test DSL
===== Custom tests and test suites
You can use idiomatic Kotlin to define your own types of tests and test suites, like this test variant with an iterations
parameter:
[source,kotlin]
fun TestSuite.test(name: String, iterations: Int, action: TestAction) { for (iteration in 1..iterations) { test("$name#$iteration") { action() } }
}
NOTE: The IDE plugin may not recognize invocations of your custom function as defining a test or test suite. In that case, xref:examples/framework-core/src/commonTest/kotlin/com/example/testLibrary/TestVariants.kt[add a @TestDiscoverable
annotation to your custom test] or test suite function.
===== Wrappers
You can use TestBalloon's wrappers for setup and tear-down code. Inside the wrappers, Kotlin idioms like withTimeout
and try
/catch
blocks can surround tests and suites.
- xref:examples/framework-core/src/commonTest/kotlin/com/example/UsingAroundAll.kt[
aroundAll
] wraps a lambda around an entire test suite. - xref:examples/framework-core/src/commonTest/kotlin/com/example/UsingAroundEach.kt[
aroundEach
] wraps a lambda around each test of a test suite (including those in child suites).
==== Extensible configuration API
You can configure your tests, test suites, and global settings through a unified, small-surface API (the TestConfig
builder). You can compose existing configurations as needed, and supply your own custom configurations.
[source,kotlin]
testConfig = TestConfig .invocation(TestInvocation.CONCURRENT) <1> .coroutineContext(dispatcherWithParallelism(4)) <2>
.statisticsReport() <3>
<1> Use concurrent test execution instead of the sequential default.
<2> Parallelize as needed (and the platform supports).
<3> A custom configuration for extra reporting
==== Global configuration, compartments
If you declare a subclass of TestSession
, its testConfig
parameter defines the global configuration for the entire compilation module. This example extends the framework's default configuration:
[source,kotlin]
class MyTestSession : TestSession(testConfig = DefaultConfiguration.statisticsReport())
To run some test suites in isolation, and/or provide them with special configuration, you can useTestCompartment
s. These group top-level test suites, with each compartment running in isolation.
TestSession
and TestCompartment
s are just special types of TestSuite
s that form the top of the test element tree.
==== Lightweight, maintainable
TestBalloon's API is fully platform-independent (everything is in the common
source set), with almost zero redundancy in its platform-specific implementations. Though powerful, TestBalloon's architecture favors simplicity and aims to avoid implicit constructs and indirection, for viable long-term maintainability.
=== Examples and documentation
Find examples demonstrating TestBalloon's capabilities in link:examples/framework-core[], and an example showing how to use TestBalloon with Kotest assertions in link:examples/integration-kotest-assertions[].
The TestBalloon public API includes source code documentation.
=== More Information
Please familiarize yourself with TestBalloon's xref:documentation/Limitations.adoc[limitations].
If you'd like to know why and how TestBalloon came to life, read about its xref:documentation/Background.adoc[background].
If you are wondering why TestBalloon works the way it does, read about its xref:documentation/Design Considerations.adoc[design considerations].
Finally, there is a brief xref:documentation/Development.adoc[introduction to development].