diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bbd8eee..f6f8ec8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,12 @@ [versions] kotlin = "1.9.0" +kotest = "5.7.1" kotlinx-coroutines = "1.7.3" kotlinx-datetime = "0.4.1" [plugins] kotlin = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotest = { id = "io.kotest.multiplatform", version.ref = "kotest" } [libraries] kotlin-test-common = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "kotlin" } @@ -14,6 +16,8 @@ kotlin-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } +kotest-engine = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" } +kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } # To use existing plugins in convention plugins, we must also declare them as libraries gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } diff --git a/runners/runner-kotest/build.gradle.kts b/runners/runner-kotest/build.gradle.kts new file mode 100644 index 0000000..7e52699 --- /dev/null +++ b/runners/runner-kotest/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + id("conventions.base") + id("conventions.kotlin") + id("conventions.library") + alias(libs.plugins.kotest) +} + +kotlin { + jvm { + testRuns.named("test") { + executionTask.configure { + useJUnitPlatform() + } + } + } + js(IR) { + browser { + testTask { + useMocha { + timeout = "1 minute" + } + } + } + } + linuxX64() + + val commonMain by sourceSets.getting { + dependencies { + api(projects.suite) + + implementation(libs.kotest.engine) + } + } + + val jvmMain by sourceSets.getting { + dependencies { + implementation(libs.kotest.runner.junit5) + } + } +} + +tasks.withType(Test::class) { + testLogging { + events("skipped", "failed", "passed") + } +} diff --git a/runners/runner-kotest/src/commonMain/kotlin/Marker.kt b/runners/runner-kotest/src/commonMain/kotlin/Marker.kt new file mode 100644 index 0000000..1b573ac --- /dev/null +++ b/runners/runner-kotest/src/commonMain/kotlin/Marker.kt @@ -0,0 +1,3 @@ +package opensavvy.prepared.runner.kotest + +class Marker diff --git a/runners/runner-kotest/src/commonTest/kotlin/KotestVanillaTest.kt b/runners/runner-kotest/src/commonTest/kotlin/KotestVanillaTest.kt new file mode 100644 index 0000000..38a43e2 --- /dev/null +++ b/runners/runner-kotest/src/commonTest/kotlin/KotestVanillaTest.kt @@ -0,0 +1,10 @@ +package opensavvy.prepared.runner.kotest + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class KotestVanillaTest : StringSpec({ + "hello world" { + "hello".length shouldBe 5 + } +}) diff --git a/settings.gradle.kts b/settings.gradle.kts index be36c51..8a17b29 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,6 +25,7 @@ include( "compat:compat-kotlinx-datetime", "runners:runner-kotlin-test", + "runners:runner-kotest", ) buildCache { -- 2.51.2 From eb22962269b53d90185d199ee74dd97d5582a930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Thu, 21 Sep 2023 22:07:35 +0200 Subject: [PATCH 2/2] feat(kotest): Implement a converter to execute Prepared tests with Kotest --- .../src/commonMain/kotlin/Marker.kt | 3 - .../src/commonMain/kotlin/PreparedSuite.kt | 64 +++++++++++++++++++ .../src/commonTest/kotlin/KotestTest.kt | 36 +++++++++++ .../commonTest/kotlin/KotestVanillaTest.kt | 10 --- 4 files changed, 100 insertions(+), 13 deletions(-) delete mode 100644 runners/runner-kotest/src/commonMain/kotlin/Marker.kt create mode 100644 runners/runner-kotest/src/commonMain/kotlin/PreparedSuite.kt create mode 100644 runners/runner-kotest/src/commonTest/kotlin/KotestTest.kt delete mode 100644 runners/runner-kotest/src/commonTest/kotlin/KotestVanillaTest.kt diff --git a/runners/runner-kotest/src/commonMain/kotlin/Marker.kt b/runners/runner-kotest/src/commonMain/kotlin/Marker.kt deleted file mode 100644 index 1b573ac..0000000 --- a/runners/runner-kotest/src/commonMain/kotlin/Marker.kt +++ /dev/null @@ -1,3 +0,0 @@ -package opensavvy.prepared.runner.kotest - -class Marker diff --git a/runners/runner-kotest/src/commonMain/kotlin/PreparedSuite.kt b/runners/runner-kotest/src/commonMain/kotlin/PreparedSuite.kt new file mode 100644 index 0000000..b5a9288 --- /dev/null +++ b/runners/runner-kotest/src/commonMain/kotlin/PreparedSuite.kt @@ -0,0 +1,64 @@ +package opensavvy.prepared.runner.kotest + +import io.kotest.core.names.TestName +import io.kotest.core.spec.KotestTestScope +import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.scopes.RootScope +import io.kotest.core.spec.style.scopes.addTest +import io.kotest.core.test.TestType +import opensavvy.prepared.suite.SuiteDsl +import opensavvy.prepared.suite.TestDsl +import opensavvy.prepared.suite.runTestDsl +import kotlin.coroutines.CoroutineContext + +/** + * Executes a Prepared [SuiteDsl] in a Kotest suite. + * + * Since Kotest cannot represent nested non-suspending test suites, all nested suites are declared at the top-level + * of the provided [RootScope]. The name of the suite is appended at the start of the tests, so the suite structure is not lost. + * + * ### Example + * + * This example uses [StringSpec], but tests can be registered using any spec. + * + * ```kotlin + * class MyTests : StringSpec({ + * "A regular Kotest test" { + * // … + * } + * + * preparedSuite { + * test("A regular Prepared test") { + * // … + * } + * + * suite("A regular Prepared test suite") { + * // … + * } + * } + * }) + * ``` + */ +@KotestTestScope +fun RootScope.preparedSuite(block: SuiteDsl.() -> Unit) { + NonNestedSuite(this).block() +} + +private class NonNestedSuite(private val root: RootScope, private val prefix: String? = null) : SuiteDsl { + override fun suite(name: String, block: SuiteDsl.() -> Unit) { + NonNestedSuite(root, prefix child name).block() + } + + override fun test(name: String, context: CoroutineContext, block: suspend TestDsl.() -> Unit) { + root.addTest(testName = TestName(name = prefix child name), disabled = false, type = TestType.Test, config = null) { + runTestDsl(name, context, block) + } + } +} + +/** + * Appends [name] at the end of `this`, handling the case where `this` is `null`. + */ +private infix fun String?.child(name: String) = + if (this != null) "$this • $name" + else name diff --git a/runners/runner-kotest/src/commonTest/kotlin/KotestTest.kt b/runners/runner-kotest/src/commonTest/kotlin/KotestTest.kt new file mode 100644 index 0000000..8901e0d --- /dev/null +++ b/runners/runner-kotest/src/commonTest/kotlin/KotestTest.kt @@ -0,0 +1,36 @@ +package opensavvy.prepared.runner.kotest + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.delay + +class KotestTest : StringSpec({ + // Vanilla Kotest declarations… + "Kotest" { + println("Done") + } + + "Hello world from Kotest" { + "hello".length shouldBe 5 + } + + // Prepared integration… + preparedSuite { + suite("Prepared") { + suite("Nested") { + test("Prepared") { + delay(1000) + println("Done") + } + + test("Other") { + println("Nothing to do") + } + } + } + + test("Hello world from Prepared") { + "hello".length shouldBe 5 + } + } +}) diff --git a/runners/runner-kotest/src/commonTest/kotlin/KotestVanillaTest.kt b/runners/runner-kotest/src/commonTest/kotlin/KotestVanillaTest.kt deleted file mode 100644 index 38a43e2..0000000 --- a/runners/runner-kotest/src/commonTest/kotlin/KotestVanillaTest.kt +++ /dev/null @@ -1,10 +0,0 @@ -package opensavvy.prepared.runner.kotest - -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe - -class KotestVanillaTest : StringSpec({ - "hello world" { - "hello".length shouldBe 5 - } -})