diff --git a/build.sbt b/build.sbt index b5f1e036..51e31591 100644 --- a/build.sbt +++ b/build.sbt @@ -73,6 +73,7 @@ lazy val root = (project in file(".")) .aggregate( core, munit, + specs2, scalatest, scalatestSelenium, jdbc, @@ -252,6 +253,15 @@ lazy val munit = (project in file("test-framework/munit")) libraryDependencies ++= Dependencies.munit.value ) +lazy val specs2 = (project in file("test-framework/specs2")) + .dependsOn(core % "compile->compile;test->test;provided->provided") + .settings(commonSettings) + .settings( + name := "testcontainers-scala-specs2", + libraryDependencies ++= Dependencies.specs2.value, + Test / scalacOptions --= Seq("-Xfatal-warnings") + ) + lazy val scalatestSelenium = (project in file("test-framework/scalatest-selenium")) .dependsOn(scalatest % "compile->compile;test->test;provided->provided") .settings(commonSettings) diff --git a/modules/gcloud/src/test/scala/com/dimafeng/testcontainers/FirestoreEmulatorContainerSpec.scala b/modules/gcloud/src/test/scala/com/dimafeng/testcontainers/FirestoreEmulatorContainerSpec.scala index c7228cea..14e2cc8c 100644 --- a/modules/gcloud/src/test/scala/com/dimafeng/testcontainers/FirestoreEmulatorContainerSpec.scala +++ b/modules/gcloud/src/test/scala/com/dimafeng/testcontainers/FirestoreEmulatorContainerSpec.scala @@ -2,12 +2,19 @@ package com.dimafeng.testcontainers import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike +import org.testcontainers.utility.DockerImageName import scala.collection.JavaConverters._ -class FirestoreEmulatorContainerSpec extends AnyWordSpecLike with Matchers with ForAllTestContainer { +class FirestoreEmulatorContainerSpec + extends AnyWordSpecLike + with Matchers + with ForAllTestContainer { - override val container: FirestoreEmulatorContainer = FirestoreEmulatorContainer() + override val container: FirestoreEmulatorContainer = + FirestoreEmulatorContainer( + DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk:382.0.0") + ) "Firestore emulator container" should { diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 9c9790f6..468c8505 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -21,6 +21,13 @@ object Dependencies { private val scalaTestSeleniumVersion_scala3 = "3.2.9.0" private val junitVersion = "4.13.2" private val munitVersion = "1.1.1" + + private val specs2Version = Def.setting { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, _)) => "4.20.5" + case _ => "5.6.4" // Scala 3 + } + } private val mysqlConnectorVersion = "5.1.42" private val neo4jConnectorVersion = "4.0.0" private val oracleDriverVersion = "21.18.0.0" @@ -82,6 +89,14 @@ object Dependencies { ) ) + val specs2 = Def.setting { + PROVIDED( + "org.specs2" %% "specs2-core" % specs2Version.value + ) ++ TEST( + "org.mockito" % "mockito-core" % mockitoVersion + ) + } + val scalatestSelenium = Def.setting( COMPILE( "org.testcontainers" % "selenium" % testcontainersVersion, diff --git a/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainerForAll.scala b/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainerForAll.scala new file mode 100644 index 00000000..6272df03 --- /dev/null +++ b/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainerForAll.scala @@ -0,0 +1,10 @@ +package com.dimafeng.testcontainers.specs2 + +import com.dimafeng.testcontainers.ContainerDef +import org.specs2.specification.core.SpecificationStructure + +trait TestContainerForAll extends TestContainersForAll { self: SpecificationStructure => + val containerDef: ContainerDef + final override type Containers = containerDef.Container + override def startContainers(): containerDef.Container = containerDef.start() +} \ No newline at end of file diff --git a/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainerForEach.scala b/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainerForEach.scala new file mode 100644 index 00000000..4edad7f7 --- /dev/null +++ b/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainerForEach.scala @@ -0,0 +1,10 @@ +package com.dimafeng.testcontainers.specs2 + +import com.dimafeng.testcontainers.ContainerDef +import org.specs2.specification.core.SpecificationStructure + +trait TestContainerForEach extends TestContainersForEach { self: SpecificationStructure => + val containerDef: ContainerDef + final override type Containers = containerDef.Container + override def startContainers(): containerDef.Container = containerDef.start() +} \ No newline at end of file diff --git a/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainersForAll.scala b/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainersForAll.scala new file mode 100644 index 00000000..86095d33 --- /dev/null +++ b/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainersForAll.scala @@ -0,0 +1,37 @@ +package com.dimafeng.testcontainers.specs2 + +import com.dimafeng.testcontainers.lifecycle.Andable +import org.specs2.execute.{AsResult, Result} +import org.specs2.specification.core.SpecificationStructure +import org.specs2.specification.{AroundEach, BeforeAfterAll} + +trait TestContainersForAll extends TestContainersSuite with BeforeAfterAll with AroundEach { self: SpecificationStructure => + + override def beforeAll(): Unit = { + val containers = startContainers() + startedContainers = Some(containers) + try { + afterContainersStart(containers) + } catch { + case e: Throwable => + stopContainers(containers) + throw e + } + } + + override def around[R: AsResult](r: => R): Result = { + startedContainers.foreach(beforeTest) + val result = AsResult(r) + val throwable = result match { + case f: org.specs2.execute.Failure => Some(f.exception) + case e: org.specs2.execute.Error => Some(e.exception) + case _ => None + } + startedContainers.foreach(afterTest(_, throwable)) + result + } + + override def afterAll(): Unit = { + startedContainers.foreach(stopContainers) + } +} \ No newline at end of file diff --git a/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainersForEach.scala b/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainersForEach.scala new file mode 100644 index 00000000..59cb24aa --- /dev/null +++ b/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainersForEach.scala @@ -0,0 +1,30 @@ +package com.dimafeng.testcontainers.specs2 + +import com.dimafeng.testcontainers.lifecycle.Andable +import org.specs2.execute.{AsResult, Result} +import org.specs2.specification.core.SpecificationStructure +import org.specs2.specification.AroundEach + +trait TestContainersForEach extends TestContainersSuite with AroundEach { self: SpecificationStructure => + + override def around[R: AsResult](r: => R): Result = { + val containers = startContainers() + startedContainers = Some(containers) + try { + afterContainersStart(containers) + beforeTest(containers) + + val result = AsResult(r) + + val throwable = result match { + case f: org.specs2.execute.Failure => Some(f.exception) + case e: org.specs2.execute.Error => Some(e.exception) + case _ => None + } + afterTest(containers, throwable) + result + } finally { + stopContainers(containers) + } + } +} \ No newline at end of file diff --git a/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainersSuite.scala b/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainersSuite.scala new file mode 100644 index 00000000..13ad858d --- /dev/null +++ b/test-framework/specs2/src/main/scala/com/dimafeng/testcontainers/specs2/TestContainersSuite.scala @@ -0,0 +1,60 @@ +package com.dimafeng.testcontainers.specs2 + +import com.dimafeng.testcontainers.implicits.DockerImageNameConverters +import com.dimafeng.testcontainers.lifecycle.{Andable, TestLifecycleAware} +import org.specs2.specification.core.SpecificationStructure +import org.junit.runner.{Description => JunitDescription} +import org.testcontainers.lifecycle.TestDescription + +trait TestContainersSuite extends DockerImageNameConverters { self: SpecificationStructure => + type Containers <: Andable + + def startContainers(): Containers + + def withContainers[A](runTest: Containers => A): A = { + val c = startedContainers.getOrElse(throw new IllegalStateException( + "'withContainers' method can't be used before all containers are started. " + + "'withContainers' method should be used only in test cases to prevent this." + )) + runTest(c) + } + + def afterContainersStart(containers: Containers): Unit = {} + def beforeContainersStop(containers: Containers): Unit = {} + + @volatile private[testcontainers] var startedContainers: Option[Containers] = None + + private[testcontainers] val suiteDescription: TestDescription = { + val description = JunitDescription.createSuiteDescription(self.getClass) + new TestDescription { + override def getTestId: String = description.getDisplayName + override def getFilesystemFriendlyName: String = s"${description.getClassName}-${description.getMethodName}" + } + } + + private[testcontainers] def beforeTest(containers: Containers): Unit = { + containers.foreach { + case container: TestLifecycleAware => container.beforeTest(suiteDescription) + case _ => // do nothing + } + } + + private[testcontainers] def afterTest(containers: Containers, throwable: Option[Throwable]): Unit = { + containers.foreach { + case container: TestLifecycleAware => container.afterTest(suiteDescription, throwable) + case _ => // do nothing + } + } + + private[testcontainers] def stopContainers(containers: Containers): Unit = { + try { + beforeContainersStop(containers) + } finally { + try { + startedContainers.foreach(_.stop()) + } finally { + startedContainers = None + } + } + } +} \ No newline at end of file diff --git a/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/SampleContainer.scala b/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/SampleContainer.scala new file mode 100644 index 00000000..e97943d3 --- /dev/null +++ b/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/SampleContainer.scala @@ -0,0 +1,34 @@ +package com.dimafeng.testcontainers.specs2 + +import java.util.Optional +import com.dimafeng.testcontainers.{ContainerDef, SingleContainer} +import com.dimafeng.testcontainers.lifecycle.TestLifecycleAware +import org.testcontainers.containers.{GenericContainer => JavaGenericContainer} +import org.testcontainers.lifecycle.{TestDescription, TestLifecycleAware => JavaTestLifecycleAware} + +case class SampleContainer(sampleJavaContainer: SampleContainer.SampleJavaContainer) + extends SingleContainer[SampleContainer.SampleJavaContainer] with TestLifecycleAware { + override implicit val container: SampleContainer.SampleJavaContainer = sampleJavaContainer + + override def beforeTest(description: TestDescription): Unit = { + container.beforeTest(description) + } + + override def afterTest(description: TestDescription, throwable: Option[Throwable]): Unit = { + container.afterTest(description, throwable.fold[Optional[Throwable]](Optional.empty())(Optional.of)) + } +} + +object SampleContainer { + class SampleJavaContainer extends JavaGenericContainer[SampleJavaContainer] with JavaTestLifecycleAware { + override def beforeTest(description: TestDescription): Unit = {} + override def afterTest(description: TestDescription, throwable: Optional[Throwable]): Unit = {} + override def start(): Unit = {} + override def stop(): Unit = {} + } + + case class Def(sampleJavaContainer: SampleJavaContainer) extends ContainerDef { + override type Container = SampleContainer + override protected def createContainer(): SampleContainer = SampleContainer(sampleJavaContainer) + } +} \ No newline at end of file diff --git a/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/TestContainerForAllSpec.scala b/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/TestContainerForAllSpec.scala new file mode 100644 index 00000000..4f9a5521 --- /dev/null +++ b/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/TestContainerForAllSpec.scala @@ -0,0 +1,138 @@ +package com.dimafeng.testcontainers.specs2 + +import com.dimafeng.testcontainers.ContainerDef +import org.specs2.mutable.Specification +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito +import org.mockito.Mockito.{mock, verify} +import org.mockito.ArgumentCaptor +import java.util.Optional +import org.specs2.specification.core.SpecificationStructure + +class TestContainerForAllSpec extends Specification { + sequential + + "TestContainerForAll" should { + "start and stop container only once" in { + val container = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new MultipleTestsSpec(SampleContainer.Def(container)) + + runSpec(spec) + + verify(container, Mockito.times(1)).start() + verify(container, Mockito.times(2)).beforeTest(any()) + verify(container, Mockito.times(2)).afterTest(any(), any()) + verify(container, Mockito.times(1)).stop() + ok + } + + "call afterContainersStart and beforeContainersStop" in { + val container = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new LifecycleSpec(SampleContainer.Def(container)) + + runSpec(spec) + + spec.afterStartCalled must beTrue + spec.beforeStopCalled must beTrue + } + + "stop container when afterContainersStart fails" in { + val container = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new FailingAfterStartSpec(SampleContainer.Def(container)) + + // Run spec but expect exception + try { + runSpec(spec) + } catch { + case _: RuntimeException => // Expected + } + + verify(container, Mockito.times(1)).start() + verify(container, Mockito.times(1)).stop() + ok + } + + "handle test failures correctly" in { + val container = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new FailingTestSpec(SampleContainer.Def(container)) + + runSpecWithFailure(spec) + + val optionalCaptor = ArgumentCaptor.forClass(classOf[Optional[Throwable]]) + verify(container, Mockito.times(1)).afterTest(any(), optionalCaptor.capture()) + optionalCaptor.getValue.isPresent must beTrue + } + } + + class MultipleTestsSpec(override val containerDef: ContainerDef) extends Specification with TestContainerForAll { + "test1" in { withContainers { _ => ok } } + "test2" in { withContainers { _ => ok } } + } + + class LifecycleSpec(override val containerDef: ContainerDef) extends Specification with TestContainerForAll { + var afterStartCalled = false + var beforeStopCalled = false + + override def afterContainersStart(containers: containerDef.Container): Unit = { + afterStartCalled = true + } + + override def beforeContainersStop(containers: containerDef.Container): Unit = { + beforeStopCalled = true + } + + "test" in { withContainers { _ => ok } } + } + + class FailingAfterStartSpec(override val containerDef: ContainerDef) extends Specification with TestContainerForAll { + override def afterContainersStart(containers: containerDef.Container): Unit = { + throw new RuntimeException("afterContainersStart failed") + } + + "test" in { withContainers { _ => ok } } + } + + class FailingTestSpec(override val containerDef: ContainerDef) extends Specification with TestContainerForAll { + "failing test" in { + withContainers { _ => + failure("Test failed") + } + } + } + + private def runSpec(spec: TestContainerForAll): Unit = { + // Manually trigger the lifecycle methods that would normally be called by specs2 runner + spec.beforeAll() + + // Simulate running test cases by calling around() for each test + // Need to check how many tests the spec has + spec match { + case _: MultipleTestsSpec => + // Has 2 tests + spec.around { org.specs2.execute.Success() } + spec.around { org.specs2.execute.Success() } + case _ => + // Has 1 test + spec.around { org.specs2.execute.Success() } + } + + spec.afterAll() + } + + private def runSpecWithFailure(spec: TestContainerForAll): Unit = { + // Manually trigger the lifecycle methods that would normally be called by specs2 runner + spec.beforeAll() + + // Simulate running a failing test + spec.around { + // Return a failure result - this simulates what happens when a test fails + failure("Test failed") + } + + spec.afterAll() + } +} \ No newline at end of file diff --git a/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/TestContainerForEachSpec.scala b/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/TestContainerForEachSpec.scala new file mode 100644 index 00000000..a361d48e --- /dev/null +++ b/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/TestContainerForEachSpec.scala @@ -0,0 +1,153 @@ +package com.dimafeng.testcontainers.specs2 + +import com.dimafeng.testcontainers.ContainerDef +import org.specs2.mutable.Specification +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito +import org.mockito.Mockito.{mock, verify} +import org.mockito.ArgumentCaptor +import java.util.Optional +import org.specs2.specification.core.SpecificationStructure + +class TestContainerForEachSpec extends Specification { + sequential + + "TestContainerForEach" should { + "start and stop container for each test" in { + val container = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new MultipleTestsSpec(SampleContainer.Def(container)) + + runSpec(spec) + + verify(container, Mockito.times(2)).start() + verify(container, Mockito.times(2)).beforeTest(any()) + verify(container, Mockito.times(2)).afterTest(any(), any()) + verify(container, Mockito.times(2)).stop() + ok + } + + "call afterContainersStart and beforeContainersStop for each test" in { + val container = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new LifecycleSpec(SampleContainer.Def(container)) + + runSpec(spec) + + spec.afterStartCount must equalTo(2) + spec.beforeStopCount must equalTo(2) + } + + "stop container when afterContainersStart fails" in { + val container = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new FailingAfterStartSpec(SampleContainer.Def(container)) + + // Run spec but expect exception + try { + runSpec(spec) + } catch { + case _: RuntimeException => // Expected + } + + verify(container, Mockito.times(1)).start() + verify(container, Mockito.times(1)).stop() + ok + } + + "stop container even when test fails" in { + val container = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new FailingTestSpec(SampleContainer.Def(container)) + + runSpec(spec) + + verify(container, Mockito.times(1)).start() + verify(container, Mockito.times(1)).stop() + + val optionalCaptor = ArgumentCaptor.forClass(classOf[Optional[Throwable]]) + verify(container, Mockito.times(1)).afterTest(any(), optionalCaptor.capture()) + optionalCaptor.getValue.isPresent must beTrue + } + + "withContainers should fail before containers are started" in { + val container = mock(classOf[SampleContainer.SampleJavaContainer]) + val spec = new WithContainersBeforeStartSpec(SampleContainer.Def(container)) + + spec.withContainers(_ => ()) must throwAn[IllegalStateException]( + message = "'withContainers' method can't be used before all containers are started" + ) + } + } + + class MultipleTestsSpec(override val containerDef: ContainerDef) extends Specification with TestContainerForEach { + "test1" in { withContainers { _ => ok } } + "test2" in { withContainers { _ => ok } } + } + + class LifecycleSpec(override val containerDef: ContainerDef) extends Specification with TestContainerForEach { + var afterStartCount = 0 + var beforeStopCount = 0 + + override def afterContainersStart(containers: containerDef.Container): Unit = { + afterStartCount += 1 + } + + override def beforeContainersStop(containers: containerDef.Container): Unit = { + beforeStopCount += 1 + } + + "test1" in { withContainers { _ => ok } } + "test2" in { withContainers { _ => ok } } + } + + class FailingAfterStartSpec(override val containerDef: ContainerDef) extends Specification with TestContainerForEach { + override def afterContainersStart(containers: containerDef.Container): Unit = { + throw new RuntimeException("afterContainersStart failed") + } + + "test" in { withContainers { _ => ok } } + } + + class FailingTestSpec(override val containerDef: ContainerDef) extends Specification with TestContainerForEach { + "failing test" in { + withContainers { _ => + failure("Test failed") + } + } + } + + class WithContainersBeforeStartSpec(override val containerDef: ContainerDef) extends Specification with TestContainerForEach { + // Empty spec to test withContainers before start + } + + private def runSpec(spec: TestContainerForEach): Unit = { + // Simulate running test cases + spec match { + case s: MultipleTestsSpec => + // Simulate running 2 tests + runSingleTest(s) + runSingleTest(s) + case s: LifecycleSpec => + // Simulate running 2 tests + runSingleTest(s) + runSingleTest(s) + case s: FailingAfterStartSpec => + // Simulate running 1 test that fails during setup + runSingleTest(s) + case s: FailingTestSpec => + // Simulate running 1 failing test + runSingleTest(s) + } + } + + private def runSingleTest(spec: TestContainerForEach): Unit = { + // Note: beforeTest and afterTest are called within the around method implementation + spec.around { + spec match { + case _: FailingTestSpec => failure("Test failed") + case _ => org.specs2.execute.Success() + } + } + } +} \ No newline at end of file diff --git a/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/TestContainersForAllSpec.scala b/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/TestContainersForAllSpec.scala new file mode 100644 index 00000000..1ad4e411 --- /dev/null +++ b/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/TestContainersForAllSpec.scala @@ -0,0 +1,169 @@ +package com.dimafeng.testcontainers.specs2 + +import com.dimafeng.testcontainers.lifecycle.and +import org.specs2.mutable.Specification +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito +import org.mockito.Mockito.{mock, verify, inOrder} +import org.mockito.ArgumentCaptor +import java.util.Optional +import org.specs2.specification.core.SpecificationStructure + +class TestContainersForAllSpec extends Specification { + sequential + + "TestContainersForAll" should { + "start and stop multiple containers only once" in { + val container1 = mock(classOf[SampleContainer.SampleJavaContainer]) + val container2 = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new MultipleContainersSpec(container1, container2) + + runSpec(spec) + + verify(container1, Mockito.times(1)).start() + verify(container2, Mockito.times(1)).start() + verify(container1, Mockito.times(2)).beforeTest(any()) + verify(container2, Mockito.times(2)).beforeTest(any()) + verify(container1, Mockito.times(2)).afterTest(any(), any()) + verify(container2, Mockito.times(2)).afterTest(any(), any()) + verify(container1, Mockito.times(1)).stop() + verify(container2, Mockito.times(1)).stop() + ok + } + + "call afterContainersStart and beforeContainersStop" in { + val container1 = mock(classOf[SampleContainer.SampleJavaContainer]) + val container2 = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new LifecycleSpec(container1, container2) + + runSpec(spec) + + spec.afterStartCalled must beTrue + spec.beforeStopCalled must beTrue + } + + "stop containers in reverse order" in { + val container1 = mock(classOf[SampleContainer.SampleJavaContainer]) + val container2 = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new MultipleContainersSpec(container1, container2) + + runSpec(spec) + + val inOrderVerifier = inOrder(container1, container2) + inOrderVerifier.verify(container1).start() + inOrderVerifier.verify(container2).start() + inOrderVerifier.verify(container2).stop() + inOrderVerifier.verify(container1).stop() + ok + } + + "access containers via pattern matching" in { + val container1 = mock(classOf[SampleContainer.SampleJavaContainer]) + val container2 = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new PatternMatchingSpec(container1, container2) + + runSpec(spec) + + spec.container1Accessed must beTrue + spec.container2Accessed must beTrue + } + } + + class MultipleContainersSpec( + sampleJavaContainer1: SampleContainer.SampleJavaContainer, + sampleJavaContainer2: SampleContainer.SampleJavaContainer + ) extends Specification with TestContainersForAll { + override type Containers = SampleContainer and SampleContainer + + override def startContainers(): Containers = { + val container1 = SampleContainer.Def(sampleJavaContainer1).start() + val container2 = SampleContainer.Def(sampleJavaContainer2).start() + container1 and container2 + } + + "test1" in { withContainers { _ => ok } } + "test2" in { withContainers { _ => ok } } + } + + class LifecycleSpec( + sampleJavaContainer1: SampleContainer.SampleJavaContainer, + sampleJavaContainer2: SampleContainer.SampleJavaContainer + ) extends Specification with TestContainersForAll { + override type Containers = SampleContainer and SampleContainer + + var afterStartCalled = false + var beforeStopCalled = false + + override def startContainers(): Containers = { + val container1 = SampleContainer.Def(sampleJavaContainer1).start() + val container2 = SampleContainer.Def(sampleJavaContainer2).start() + container1 and container2 + } + + override def afterContainersStart(containers: Containers): Unit = { + afterStartCalled = true + } + + override def beforeContainersStop(containers: Containers): Unit = { + beforeStopCalled = true + } + + "test" in { withContainers { _ => ok } } + } + + class PatternMatchingSpec( + val sampleJavaContainer1: SampleContainer.SampleJavaContainer, + val sampleJavaContainer2: SampleContainer.SampleJavaContainer + ) extends Specification with TestContainersForAll { + override type Containers = SampleContainer and SampleContainer + + var container1Accessed = false + var container2Accessed = false + + override def startContainers(): Containers = { + val container1 = SampleContainer.Def(sampleJavaContainer1).start() + val container2 = SampleContainer.Def(sampleJavaContainer2).start() + container1 and container2 + } + + "access containers" in { + withContainers { case c1 and c2 => + container1Accessed = c1.sampleJavaContainer == sampleJavaContainer1 + container2Accessed = c2.sampleJavaContainer == sampleJavaContainer2 + ok + } + } + } + + private def runSpec(spec: TestContainersForAll): Unit = { + // Manually trigger the lifecycle methods that would normally be called by specs2 runner + spec.beforeAll() + + // Simulate running test cases by calling around() for each test + // Need to check how many tests the spec has + spec match { + case _: MultipleContainersSpec => + // Has 2 tests + spec.around { org.specs2.execute.Success() } + spec.around { org.specs2.execute.Success() } + case pm: PatternMatchingSpec => + // Has 1 test that needs to actually run the pattern matching code + spec.around { + pm.withContainers { case c1 and c2 => + pm.container1Accessed = c1.sampleJavaContainer == pm.sampleJavaContainer1 + pm.container2Accessed = c2.sampleJavaContainer == pm.sampleJavaContainer2 + org.specs2.execute.Success() + } + } + case _ => + // Has 1 test + spec.around { org.specs2.execute.Success() } + } + + spec.afterAll() + } +} \ No newline at end of file diff --git a/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/TestContainersForEachSpec.scala b/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/TestContainersForEachSpec.scala new file mode 100644 index 00000000..97a2bab7 --- /dev/null +++ b/test-framework/specs2/src/test/scala/com/dimafeng/testcontainers/specs2/TestContainersForEachSpec.scala @@ -0,0 +1,258 @@ +package com.dimafeng.testcontainers.specs2 + +import com.dimafeng.testcontainers.lifecycle.and +import org.specs2.mutable.Specification +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito +import org.mockito.Mockito.{mock, verify, inOrder} +import org.mockito.ArgumentCaptor +import java.util.Optional +import org.specs2.specification.core.SpecificationStructure + +class TestContainersForEachSpec extends Specification { + sequential + + "TestContainersForEach" should { + "start and stop multiple containers for each test" in { + val container1 = mock(classOf[SampleContainer.SampleJavaContainer]) + val container2 = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new MultipleContainersSpec(container1, container2) + + runSpec(spec) + + verify(container1, Mockito.times(2)).start() + verify(container2, Mockito.times(2)).start() + verify(container1, Mockito.times(2)).beforeTest(any()) + verify(container2, Mockito.times(2)).beforeTest(any()) + verify(container1, Mockito.times(2)).afterTest(any(), any()) + verify(container2, Mockito.times(2)).afterTest(any(), any()) + verify(container1, Mockito.times(2)).stop() + verify(container2, Mockito.times(2)).stop() + ok + } + + "call afterContainersStart and beforeContainersStop for each test" in { + val container1 = mock(classOf[SampleContainer.SampleJavaContainer]) + val container2 = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new LifecycleSpec(container1, container2) + + runSpec(spec) + + spec.afterStartCount must equalTo(2) + spec.beforeStopCount must equalTo(2) + } + + "stop containers in reverse order for each test" in { + val container1 = mock(classOf[SampleContainer.SampleJavaContainer]) + val container2 = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new SingleTestSpec(container1, container2) + + runSpec(spec) + + val inOrderVerifier = inOrder(container1, container2) + inOrderVerifier.verify(container1).start() + inOrderVerifier.verify(container2).start() + inOrderVerifier.verify(container2).stop() + inOrderVerifier.verify(container1).stop() + ok + } + + "stop containers even when test fails" in { + val container1 = mock(classOf[SampleContainer.SampleJavaContainer]) + val container2 = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new FailingTestSpec(container1, container2) + + runSpec(spec) + + verify(container1, Mockito.times(1)).start() + verify(container2, Mockito.times(1)).start() + verify(container1, Mockito.times(1)).stop() + verify(container2, Mockito.times(1)).stop() + + val optionalCaptor = ArgumentCaptor.forClass(classOf[Optional[Throwable]]) + verify(container1, Mockito.times(1)).afterTest(any(), optionalCaptor.capture()) + optionalCaptor.getValue.isPresent must beTrue + } + + "access containers via pattern matching" in { + val container1 = mock(classOf[SampleContainer.SampleJavaContainer]) + val container2 = mock(classOf[SampleContainer.SampleJavaContainer]) + + val spec = new PatternMatchingSpec(container1, container2) + + runSpec(spec) + + spec.container1Accessed must beTrue + spec.container2Accessed must beTrue + } + + "withContainers should fail before containers are started" in { + val container1 = mock(classOf[SampleContainer.SampleJavaContainer]) + val container2 = mock(classOf[SampleContainer.SampleJavaContainer]) + val spec = new WithContainersBeforeStartSpec(container1, container2) + + spec.withContainers(_ => ()) must throwAn[IllegalStateException]( + message = "'withContainers' method can't be used before all containers are started" + ) + } + } + + class MultipleContainersSpec( + sampleJavaContainer1: SampleContainer.SampleJavaContainer, + sampleJavaContainer2: SampleContainer.SampleJavaContainer + ) extends Specification with TestContainersForEach { + override type Containers = SampleContainer and SampleContainer + + override def startContainers(): Containers = { + val container1 = SampleContainer.Def(sampleJavaContainer1).start() + val container2 = SampleContainer.Def(sampleJavaContainer2).start() + container1 and container2 + } + + "test1" in { withContainers { _ => ok } } + "test2" in { withContainers { _ => ok } } + } + + class SingleTestSpec( + sampleJavaContainer1: SampleContainer.SampleJavaContainer, + sampleJavaContainer2: SampleContainer.SampleJavaContainer + ) extends Specification with TestContainersForEach { + override type Containers = SampleContainer and SampleContainer + + override def startContainers(): Containers = { + val container1 = SampleContainer.Def(sampleJavaContainer1).start() + val container2 = SampleContainer.Def(sampleJavaContainer2).start() + container1 and container2 + } + + "test" in { withContainers { _ => ok } } + } + + class LifecycleSpec( + sampleJavaContainer1: SampleContainer.SampleJavaContainer, + sampleJavaContainer2: SampleContainer.SampleJavaContainer + ) extends Specification with TestContainersForEach { + override type Containers = SampleContainer and SampleContainer + + var afterStartCount = 0 + var beforeStopCount = 0 + + override def startContainers(): Containers = { + val container1 = SampleContainer.Def(sampleJavaContainer1).start() + val container2 = SampleContainer.Def(sampleJavaContainer2).start() + container1 and container2 + } + + override def afterContainersStart(containers: Containers): Unit = { + afterStartCount += 1 + } + + override def beforeContainersStop(containers: Containers): Unit = { + beforeStopCount += 1 + } + + "test1" in { withContainers { _ => ok } } + "test2" in { withContainers { _ => ok } } + } + + class FailingTestSpec( + sampleJavaContainer1: SampleContainer.SampleJavaContainer, + sampleJavaContainer2: SampleContainer.SampleJavaContainer + ) extends Specification with TestContainersForEach { + override type Containers = SampleContainer and SampleContainer + + override def startContainers(): Containers = { + val container1 = SampleContainer.Def(sampleJavaContainer1).start() + val container2 = SampleContainer.Def(sampleJavaContainer2).start() + container1 and container2 + } + + "failing test" in { + withContainers { _ => + failure("Test failed") + } + } + } + + class PatternMatchingSpec( + val sampleJavaContainer1: SampleContainer.SampleJavaContainer, + val sampleJavaContainer2: SampleContainer.SampleJavaContainer + ) extends Specification with TestContainersForEach { + override type Containers = SampleContainer and SampleContainer + + var container1Accessed = false + var container2Accessed = false + + override def startContainers(): Containers = { + val container1 = SampleContainer.Def(sampleJavaContainer1).start() + val container2 = SampleContainer.Def(sampleJavaContainer2).start() + container1 and container2 + } + + "access containers" in { + withContainers { case c1 and c2 => + container1Accessed = c1.sampleJavaContainer == sampleJavaContainer1 + container2Accessed = c2.sampleJavaContainer == sampleJavaContainer2 + ok + } + } + } + + class WithContainersBeforeStartSpec( + sampleJavaContainer1: SampleContainer.SampleJavaContainer, + sampleJavaContainer2: SampleContainer.SampleJavaContainer + ) extends Specification with TestContainersForEach { + override type Containers = SampleContainer and SampleContainer + + override def startContainers(): Containers = { + val container1 = SampleContainer.Def(sampleJavaContainer1).start() + val container2 = SampleContainer.Def(sampleJavaContainer2).start() + container1 and container2 + } + + // Empty spec to test withContainers before start + } + + private def runSpec(spec: TestContainersForEach): Unit = { + // Simulate running test cases + spec match { + case s: MultipleContainersSpec => + // Simulate running 2 tests + runSingleTest(s) + runSingleTest(s) + case s: LifecycleSpec => + // Simulate running 2 tests + runSingleTest(s) + runSingleTest(s) + case s: SingleTestSpec => + // Simulate running 1 test + runSingleTest(s) + case s: FailingTestSpec => + // Simulate running 1 failing test + runSingleTest(s) + case s: PatternMatchingSpec => + // Simulate running 1 test with pattern matching + s.around { + s.withContainers { case c1 and c2 => + s.container1Accessed = c1.sampleJavaContainer == s.sampleJavaContainer1 + s.container2Accessed = c2.sampleJavaContainer == s.sampleJavaContainer2 + org.specs2.execute.Success() + } + } + } + } + + private def runSingleTest(spec: TestContainersForEach): Unit = { + // Note: beforeTest and afterTest are called within the around method implementation + spec.around { + spec match { + case _: FailingTestSpec => failure("Test failed") + case _ => org.specs2.execute.Success() + } + } + } +} \ No newline at end of file