diff --git a/modules/kotest/build.gradle b/modules/kotest/build.gradle new file mode 100644 index 00000000000..393b72ee5fc --- /dev/null +++ b/modules/kotest/build.gradle @@ -0,0 +1,38 @@ +plugins { + id "org.jetbrains.kotlin.jvm" version "1.3.71" + id "org.jetbrains.kotlin.kapt" version "1.3.71" +} + +ext { + kotestVersion = "4.0.3" +} + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile project(':testcontainers') + api("io.kotest:kotest-runner-junit5-jvm:$kotestVersion") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + testImplementation("io.kotest:kotest-assertions-core-jvm:$kotestVersion") +} + + +test { + useJUnitPlatform() +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + javaParameters = true + } +} +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} diff --git a/modules/kotest/src/main/kotlin/org/testcontainers/containers/Extensions.kt b/modules/kotest/src/main/kotlin/org/testcontainers/containers/Extensions.kt new file mode 100644 index 00000000000..2701a2e601e --- /dev/null +++ b/modules/kotest/src/main/kotlin/org/testcontainers/containers/Extensions.kt @@ -0,0 +1,7 @@ +package org.testcontainers.containers + +import io.kotest.core.listeners.TestListener +import org.testcontainers.lifecycle.Startable + +fun Startable.perTest(): TestListener = StartablePerTestListener(this) +fun Startable.perSpec(): TestListener = StartablePerSpecListener(this) diff --git a/modules/kotest/src/main/kotlin/org/testcontainers/containers/StartablePerSpecListener.kt b/modules/kotest/src/main/kotlin/org/testcontainers/containers/StartablePerSpecListener.kt new file mode 100644 index 00000000000..fb66f608ff6 --- /dev/null +++ b/modules/kotest/src/main/kotlin/org/testcontainers/containers/StartablePerSpecListener.kt @@ -0,0 +1,52 @@ +package org.testcontainers.containers + +import io.kotest.core.listeners.TestListener +import io.kotest.core.spec.Spec +import io.kotest.core.test.TestCase +import io.kotest.core.test.TestResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.testcontainers.lifecycle.Startable +import org.testcontainers.lifecycle.TestLifecycleAware + + +/** + * [StartablePerSpecListener] starts the given [startable] before execution of any test in the spec + * and stops after execution of all tests. If the [startable] also inherit from [TestLifecycleAware] + * then its [beforeTest] and [afterTest] method are also called by the listener. + * + * [startable] can any of [GenericContainer] [DockerComposeContainer] [LocalStackContainer] etc. + * + * This listener should be used when you want to use a single container for all tests in a single spec class. + * + * @see + * [StartablePerTestListener] + * */ + +internal class StartablePerSpecListener(private val startable: Startable) : TestListener { + private val testLifecycleAwareListener = TestLifecycleAwareListener(startable) + + override suspend fun beforeSpec(spec: Spec) { + withContext(Dispatchers.IO) { + startable.start() + } + } + + override suspend fun beforeTest(testCase: TestCase) { + withContext(Dispatchers.IO) { + testLifecycleAwareListener.beforeTest(testCase) + } + } + + override suspend fun afterSpec(spec: Spec) { + withContext(Dispatchers.IO) { + startable.stop() + } + } + + override suspend fun afterTest(testCase: TestCase, result: TestResult) { + withContext(Dispatchers.IO) { + testLifecycleAwareListener.afterTest(testCase, result) + } + } +} diff --git a/modules/kotest/src/main/kotlin/org/testcontainers/containers/StartablePerTestListener.kt b/modules/kotest/src/main/kotlin/org/testcontainers/containers/StartablePerTestListener.kt new file mode 100644 index 00000000000..43cb3c338e8 --- /dev/null +++ b/modules/kotest/src/main/kotlin/org/testcontainers/containers/StartablePerTestListener.kt @@ -0,0 +1,40 @@ +package org.testcontainers.containers + +import io.kotest.core.listeners.TestListener +import io.kotest.core.test.TestCase +import io.kotest.core.test.TestResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.testcontainers.lifecycle.Startable +import org.testcontainers.lifecycle.TestLifecycleAware + + +/** + * [StartablePerTestListener] starts the given [startable] before execution of each test in the spec + * and stops after execution of each test. If the [startable] also inherit from [TestLifecycleAware] + * then its [beforeTest] and [afterTest] method are also called by the listener. + * + * [startable] can any of [GenericContainer] [DockerComposeContainer] [LocalStackContainer] etc. + * + * This should be used when you want a fresh container for each test. + * + * @see[StartablePerSpecListener] + * */ +internal class StartablePerTestListener(private val startable: Startable) : TestListener { + private val testLifecycleAwareListener = TestLifecycleAwareListener(startable) + + override suspend fun beforeTest(testCase: TestCase) { + withContext(Dispatchers.IO) { + testLifecycleAwareListener.beforeTest(testCase) + startable.start() + } + } + + override suspend fun afterTest(testCase: TestCase, result: TestResult) { + withContext(Dispatchers.IO) { + testLifecycleAwareListener.afterTest(testCase, result) + startable.stop() + } + } +} + diff --git a/modules/kotest/src/main/kotlin/org/testcontainers/containers/TestLifecycleAwareListener.kt b/modules/kotest/src/main/kotlin/org/testcontainers/containers/TestLifecycleAwareListener.kt new file mode 100644 index 00000000000..1640a58f33b --- /dev/null +++ b/modules/kotest/src/main/kotlin/org/testcontainers/containers/TestLifecycleAwareListener.kt @@ -0,0 +1,33 @@ +package org.testcontainers.containers + +import io.kotest.core.listeners.TestListener +import io.kotest.core.test.TestCase +import io.kotest.core.test.TestResult +import org.testcontainers.lifecycle.Startable +import org.testcontainers.lifecycle.TestDescription +import org.testcontainers.lifecycle.TestLifecycleAware +import java.net.URLEncoder +import java.util.* + +internal class TestLifecycleAwareListener(startable: Startable) : TestListener { + private val testLifecycleAware = startable as? TestLifecycleAware + + override suspend fun beforeTest(testCase: TestCase) { + testLifecycleAware?.beforeTest(testCase.toTestDescription()) + } + + override suspend fun afterTest(testCase: TestCase, result: TestResult) { + testLifecycleAware?.afterTest(testCase.toTestDescription(), Optional.ofNullable(result.error)) + } +} + +private fun TestCase.toTestDescription() = object : TestDescription { + + override fun getFilesystemFriendlyName(): String { + return URLEncoder.encode(name, "UTF-8") + } + + override fun getTestId(): String { + return description.id() + } +} diff --git a/modules/kotest/src/test/kotlin/org/testcontainers/containers/StartableTestLifecycleAware.kt b/modules/kotest/src/test/kotlin/org/testcontainers/containers/StartableTestLifecycleAware.kt new file mode 100644 index 00000000000..eff76f6a5db --- /dev/null +++ b/modules/kotest/src/test/kotlin/org/testcontainers/containers/StartableTestLifecycleAware.kt @@ -0,0 +1,16 @@ +package org.testcontainers.containers + +import org.testcontainers.lifecycle.TestDescription +import org.testcontainers.lifecycle.TestLifecycleAware +import java.util.* + +internal class StartableTestLifecycleAware : TestStartable(), TestLifecycleAware { + val testDescriptions = mutableListOf() + + override fun beforeTest(description: TestDescription?) { + testDescriptions.add(description) + } + + override fun afterTest(description: TestDescription?, throwable: Optional?) { + } +} diff --git a/modules/kotest/src/test/kotlin/org/testcontainers/containers/StartableTestLifecycleAwareTest.kt b/modules/kotest/src/test/kotlin/org/testcontainers/containers/StartableTestLifecycleAwareTest.kt new file mode 100644 index 00000000000..cfec9feb538 --- /dev/null +++ b/modules/kotest/src/test/kotlin/org/testcontainers/containers/StartableTestLifecycleAwareTest.kt @@ -0,0 +1,21 @@ +package org.testcontainers.containers + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldHaveSize + +class StartableTestLifecycleAwareTest : StringSpec({ + val startableTestLifecycleAwareForPerTest = StartableTestLifecycleAware() + val startableTestLifecycleAwareForPerSpec = StartableTestLifecycleAware() + + listeners(startableTestLifecycleAwareForPerTest.perTest(), startableTestLifecycleAwareForPerSpec.perSpec()) + + "beforeTestCount for first test should be one" { + startableTestLifecycleAwareForPerTest.testDescriptions shouldHaveSize 1 + startableTestLifecycleAwareForPerSpec.testDescriptions shouldHaveSize 1 + } + + "beforeTestCount for second test should be two" { + startableTestLifecycleAwareForPerTest.testDescriptions shouldHaveSize 2 + startableTestLifecycleAwareForPerTest.testDescriptions shouldHaveSize 2 + } +}) diff --git a/modules/kotest/src/test/kotlin/org/testcontainers/containers/TestContainerKotestIntegrationPerTest.kt b/modules/kotest/src/test/kotlin/org/testcontainers/containers/TestContainerKotestIntegrationPerTest.kt new file mode 100644 index 00000000000..5670ebbf228 --- /dev/null +++ b/modules/kotest/src/test/kotlin/org/testcontainers/containers/TestContainerKotestIntegrationPerTest.kt @@ -0,0 +1,17 @@ +package org.testcontainers.containers + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class TestContainerKotestIntegrationPerTest : StringSpec({ + val testStartable = TestStartable() + listeners(testStartable.perTest()) + + "start count for first test should be one" { + testStartable.startCount shouldBe 1 + } + + "start count for second test should be two" { + testStartable.startCount shouldBe 2 + } +}) diff --git a/modules/kotest/src/test/kotlin/org/testcontainers/containers/TestLifecycleAwareListenerTest.kt b/modules/kotest/src/test/kotlin/org/testcontainers/containers/TestLifecycleAwareListenerTest.kt new file mode 100644 index 00000000000..a2236b334c0 --- /dev/null +++ b/modules/kotest/src/test/kotlin/org/testcontainers/containers/TestLifecycleAwareListenerTest.kt @@ -0,0 +1,30 @@ +package org.testcontainers.containers + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class TestLifecycleAwareListenerTest : StringSpec({ + val startableTestLifecycleAware = StartableTestLifecycleAware() + val startable = TestStartable() + val testLifecycleAwareListener = TestLifecycleAwareListener(startableTestLifecycleAware) + val anotherTestLifecycleAwareListener = TestLifecycleAwareListener(startable) + + listeners(testLifecycleAwareListener, anotherTestLifecycleAwareListener) + + "it should not break for listener having startable which is not of type testLifecycleAware" { + startable.startCount shouldBe 0 + } + + "test id in test description should be combination of test name and package name" { + val testDescription = startableTestLifecycleAware.testDescriptions[1] + testDescription?.testId shouldBe "org.testcontainers.containers.TestLifecycleAwareListenerTest/test id in test" + + " description should be combination of test name and package name" + } + + "fileSystemFriendlyName .. in /// test description should be encoded test name" { + val testDescription = startableTestLifecycleAware.testDescriptions[2] + val encodedTestName = "fileSystemFriendlyName+..+in+%2F%2F%2F+test+description+should+be+encoded+test+name" + + testDescription?.filesystemFriendlyName shouldBe encodedTestName + } +}) diff --git a/modules/kotest/src/test/kotlin/org/testcontainers/containers/TestStartable.kt b/modules/kotest/src/test/kotlin/org/testcontainers/containers/TestStartable.kt new file mode 100644 index 00000000000..32abd20af06 --- /dev/null +++ b/modules/kotest/src/test/kotlin/org/testcontainers/containers/TestStartable.kt @@ -0,0 +1,12 @@ +package org.testcontainers.containers + +import org.testcontainers.lifecycle.Startable + +internal open class TestStartable : Startable { + var startCount = 0 + override fun start() { + startCount++ + } + override fun stop() { + } +} diff --git a/modules/kotest/src/test/kotlin/org/testcontainers/containers/TestcontainerKotestIntegrationPerSpec.kt b/modules/kotest/src/test/kotlin/org/testcontainers/containers/TestcontainerKotestIntegrationPerSpec.kt new file mode 100644 index 00000000000..2d14f696c82 --- /dev/null +++ b/modules/kotest/src/test/kotlin/org/testcontainers/containers/TestcontainerKotestIntegrationPerSpec.kt @@ -0,0 +1,17 @@ +package org.testcontainers.containers + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class TestcontainerKotestIntegrationPerSpec : StringSpec({ + val testStartable = TestStartable() + listeners(testStartable.perSpec()) + + "start count for first test should be one" { + testStartable.startCount shouldBe 1 + } + + "start count for second test should also be one" { + testStartable.startCount shouldBe 1 + } +})