Regression Testing

Chassis chose Kotest as TestFramework, but ordinary JUnit5 Tests also work.

(Chassis is not really using DI (Dependency Injection) by now, but evaluated it using Koin.
Despite the Code atm not using DI extensively (as passing a Context with kotlin context(CtxWrapper) proved kind of enough by now)
Chassis is prepared to use Koin DI in the future.

Especially the Testsuite is prepared to use Kotest with Koin in the Behaviour Driven Style.

Shared gradle TestFixures

For BDD Kotests to be more convenient the chassis /shared/build.gradle.kts provides TestFixtures in shared/src/testFixtures/kotlin/com/hoffi/chassis/shared/test.

Namely:

package com.hoffi.chassis.shared.test
 
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.koin.KoinExtension
import io.kotest.koin.KoinLifecycleMode
import org.koin.test.KoinTest
 
@Suppress("UNCHECKED_CAST")
abstract class KoinBddSpec(val koinModules: List<org.koin.core.module.Module>, behaviorSpec: KoinBddSpec.() -> Unit): KoinTest, BehaviorSpec(behaviorSpec as BehaviorSpec.() -> Unit) {
    constructor(vararg koinModules: org.koin.core.module.Module, behaviorSpec: KoinBddSpec.() -> Unit) : this(koinModules.asList(), behaviorSpec)
    override fun extensions() = koinModules.map { KoinExtension(module = it, mockProvider = null, mode = KoinLifecycleMode.Root) }
}

And the KotestProjectConfig see Kotest Project Level Config

package com.hoffi.chassis.shared.test
 
import io.kotest.common.ExperimentalKotest
import io.kotest.core.config.AbstractProjectConfig
import io.kotest.core.config.LogLevel
import io.kotest.core.extensions.Extension
import io.kotest.core.test.TestCase
import io.kotest.engine.test.logging.LogEntry
import io.kotest.engine.test.logging.LogExtension
 
class KotestProjectConfig : AbstractProjectConfig() {
    override val globalAssertSoftly = true
    override val logLevel = LogLevel.Info
 
    override fun extensions(): List<Extension> = listOf(
        object : LogExtension {
            override suspend fun handleLogs(testCase: TestCase, logs: List<LogEntry>) {
                logs.forEach { println(it.level.name + " - " + it.message) }
            }
        }
    )
 
    override suspend fun beforeProject() {
        println("kotests: (beforeProject() of ${this::class.simpleName})")
    }
 
    override suspend fun afterProject() {
        println("kotests finished. (afterProject() of ${this::class.simpleName})")
    }
}

Gradle subProjects use these by declaring a special dependencies { ... } dependency:

dependencies {
    testFixturesImplementation(libs.bundles.testJunitKotestKoin)
}

buildLogic/libs.versions.toml:

[libraries]
kointest = { module = "io.insert-koin:koin-test", version.ref = "kointest" }
kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
kotest-extensions-koin = { module = "io.kotest.extensions:kotest-extensions-koin", version.ref = "kotest-extensions-koin" }
kotest-framework-dataset = { module = "io.kotest:kotest-framework-datatest", version.ref = "kotest" }
kotest-framework-engine = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" }
kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
 
[bundles]
testJunitKotestKoin = [
    "kointest",
    "kotest-assertions-core",
    "kotest-extensions-koin",
    "kotest-framework-dataset",
    "kotest-framework-engine",
]

Unfortunately I did not (yet) find a way to use src/test/resources/kotest.properties from the shared project to the other subprojects)
ergo the resources/kotest.properties for testing is unix softlinked in all subprojects!!!

Kotest with Koin DI (usaging of shared TestFixture’s KoinBddSpec)

The abstract class KoinBddSpec above enables you to write Kotest Koin BDD Specs with minimal boilerplate code on using Koin DI modules:

import io.kotest.common.ExperimentalKotest
import io.kotest.engine.test.logging.info
import io.kotest.matchers.string.shouldEndWith
import io.kotest.matchers.string.shouldNotEndWith
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.module.dsl.bind
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import org.koin.test.inject
 
// Kotest Koin BDD Test
class SmokeKoinBddSpec : KoinBddSpec(dummyModule, behaviorSpec = {
    Given("an injected Dummy") {
        val dummy: Dummy by inject()
        When("calling f() on injected Dummy") {
            info { "${SmokeKoinBddSpec::class.simpleName}: When(1)"}
            val result = dummy.f()
            Then("result should be 1") {
                result shouldEndWith "with DummyDep(dp='depDummy', depDummy(1))"
            }
            Then("result should not be 2") {
                result shouldNotEndWith  "with DummyDep(dp='depDummy', depDummy(2))"
            }
        }
        When("calling f() again on injected Dummy") {
            info { "${SmokeKoinBddSpec::class.simpleName}: When(2)"}
            val result = dummy.f()
            Then("result should be 2") {
                result shouldEndWith "with DummyDep(dp='depDummy', depDummy(2))"
            }
        }
    }
})
 
// ===============================================================
// Fake stuff to demonstrate Kotest with Koin DI in BDD Spec Style
// ===============================================================
 
/** Koin dummy DI module */
val dummyModule = module {
    factory { params -> DoIt(get(), params.get()) }
    singleOf(::Dummy) { bind<IDummy>() }
    singleOf(::DummyDep)
}
 
interface IDummy {
    val p: String
    fun f(): String
}
class Dummy (val dummyDep: DummyDep): IDummy {
    //actual val dummyDep = dummyDep
    override val p: String = "JVM"
    override fun f(): String {
        return "$p with $dummyDep"
    }
}
class DummyDep {
    override fun toString() = "DummyDep(dp='$dp', ${f()})"
    val dp: String = "depDummy"
    fun f() = "$dp(${count++})"
 
    companion object {
        var count = 1L
    }
}
 
data class Par(val par: String)
 
class DoIt(val dummy: Dummy, val par: Par) : KoinComponent {
    private val otherDummy: Dummy by inject()
    fun doIt() {
        println("par='${par}' ${dummy.f()}")
        println("par='${par}' ${otherDummy.f()}")
    }
}

Caveat Kotest does not find Kotests

You have to install the intellij Kotest plugin

for gradle to find Kotests you have to explicitly useJunitPlatform() in your build.gradle.kts tests configuration,
otherwise it will only find JUnit Tests. (> No tests found)

kotlin {
    jvmToolchain(BuildLogicGlobal.jdkVersion)
    tasks.withType<Test>().configureEach {
        // since gradle 8.x JunitPlatform is the default and must not be configured explicitly anymore
        useJUnitPlatform() // but if missing this line, kotlin kotests won't be found and run TODO
        failFast = false
    }
}

back to root