Skip to content

TestBalloon is a coroutines-powered test framework providing structured testing for Kotlin Multiplatform. It is lightweight, heavy-lifting, and easy to use (like a balloon).

License

Notifications You must be signed in to change notification settings

infix-de/testBalloon

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TestBalloon logo TestBalloon

Structured testing for Kotlin Multiplatform

TestBalloon is a coroutines-powered test framework. It is lightweight, heavy-lifting, and easy to use (like a balloon).
Example Test Run

How to use TestBalloon

  1. Add the TestBalloon Gradle plugin to your build script:

    plugins {
        id("de.infix.testBalloon") version "VERSION"
    }
  2. Add a dependency for the TestBalloon framework core library:

    commonTest {
        dependencies {
            implementation("de.infix.testBalloon:testBalloon-framework-core:VERSION")
        }
    }
  3. Add a dependency for the assertion library of your choice.

    • For kotlin-test assertions:

      implementation(kotlin("test"))
    • For Kotest assertions with support for soft assertion and clues:

      implementation("de.infix.testBalloon:testBalloon-integration-kotest-assertions:VERSION")
  4. Write a test:

    val MyFirstTestSuite by testSuite {
        test("string length") {
            assertEquals(8, "Test me!".length)
        }
    }
  5. Run tests via the familiar Gradle test targets.

  6. 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.

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.

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:

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:

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, 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.

  • aroundAll wraps a lambda around an entire test suite.

  • 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.

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:

class MyTestSession : TestSession(testConfig = DefaultConfiguration.statisticsReport())

To run some test suites in isolation, and/or provide them with special configuration, you can useTestCompartments. These group top-level test suites, with each compartment running in isolation.

TestSession and TestCompartments are just special types of TestSuites 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 examples/framework-core, and an example showing how to use TestBalloon with Kotest assertions in examples/integration-kotest-assertions.

The TestBalloon public API includes source code documentation.

More Information

Please familiarize yourself with TestBalloon’s limitations.

If you’d like to know why and how TestBalloon came to life, read about its background.

If you are wondering why TestBalloon works the way it does, read about its design considerations.

Finally, there is a brief introduction to development.

About

TestBalloon is a coroutines-powered test framework providing structured testing for Kotlin Multiplatform. It is lightweight, heavy-lifting, and easy to use (like a balloon).

Resources

License

Stars

Watchers

Forks

Languages