diff --git a/README.md b/README.md index 31e0de65..fde272a1 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,11 @@ class MysqlSpec extends FlatSpec with ForAllTestContainer { ## Release notes +* **0.27.0** + * New `TestLifecycleAware` trait introduced. You can use it when you want to do something with the container before or after the test. + * `Container` now implements `Startable` interface with `start` and `stop` methods. + * Old container's lifecycle methods `finished`, `succeeded`, `starting`, `failed` are deprecated. Use `start`, `stop`, and `TestLifecycleAware` methods instead. + * **0.26.0** * TestContainers `1.11.2` -> `1.11.3` * Scala 2.13.0 diff --git a/src/main/scala/com/dimafeng/testcontainers/DockerComposeContainer.scala b/src/main/scala/com/dimafeng/testcontainers/DockerComposeContainer.scala index 22064de4..234aff06 100644 --- a/src/main/scala/com/dimafeng/testcontainers/DockerComposeContainer.scala +++ b/src/main/scala/com/dimafeng/testcontainers/DockerComposeContainer.scala @@ -111,4 +111,7 @@ class DockerComposeContainer (composeFiles: ComposeFile, def getServicePort(serviceName: String, servicePort: Int): Int = container.getServicePort(serviceName, servicePort) -} \ No newline at end of file + override def start(): Unit = container.start() + + override def stop(): Unit = container.stop() +} diff --git a/src/main/scala/com/dimafeng/testcontainers/MultipleContainers.scala b/src/main/scala/com/dimafeng/testcontainers/MultipleContainers.scala index eaa863fe..e791c1db 100644 --- a/src/main/scala/com/dimafeng/testcontainers/MultipleContainers.scala +++ b/src/main/scala/com/dimafeng/testcontainers/MultipleContainers.scala @@ -1,18 +1,36 @@ package com.dimafeng.testcontainers +import com.dimafeng.testcontainers.lifecycle.TestLifecycleAware import org.junit.runner.Description +import org.testcontainers.lifecycle.TestDescription import scala.language.implicitConversions -class MultipleContainers private(containers: Seq[LazyContainer[_]]) extends Container { +class MultipleContainers private(containers: Seq[LazyContainer[_]]) extends Container with TestLifecycleAware { + @deprecated("Use `stop` instead") override def finished()(implicit description: Description): Unit = containers.foreach(_.finished()(description)) + @deprecated("Use `stop` and/or `TestLifecycleAware.afterTest` instead") override def succeeded()(implicit description: Description): Unit = containers.foreach(_.succeeded()(description)) + @deprecated("Use `start` instead") override def starting()(implicit description: Description): Unit = containers.foreach(_.starting()(description)) + @deprecated("Use `stop` and/or `TestLifecycleAware.afterTest` instead") override def failed(e: Throwable)(implicit description: Description): Unit = containers.foreach(_.failed(e)(description)) + + override def beforeTest(description: TestDescription): Unit = { + containers.foreach(_.beforeTest(description)) + } + + override def afterTest(description: TestDescription, throwable: Option[Throwable]): Unit = { + containers.foreach(_.afterTest(description, throwable)) + } + + override def start(): Unit = containers.foreach(_.start()) + + override def stop(): Unit = containers.foreach(_.stop()) } object MultipleContainers { @@ -47,16 +65,38 @@ object MultipleContainers { * You don't need to wrap your containers into the `LazyContainer` manually * when you pass your containers in the `MultipleContainers`- there is implicit conversion for that. */ -class LazyContainer[T <: Container](factory: => T) extends Container { +class LazyContainer[T <: Container](factory: => T) extends Container with TestLifecycleAware { lazy val container: T = factory + @deprecated("Use `stop` instead") override def finished()(implicit description: Description): Unit = container.finished + @deprecated("Use `stop` and/or `TestLifecycleAware.afterTest` instead") override def failed(e: Throwable)(implicit description: Description): Unit = container.failed(e) + @deprecated("Use `start` instead") override def starting()(implicit description: Description): Unit = container.starting() + @deprecated("Use `stop` and/or `TestLifecycleAware.afterTest` instead") override def succeeded()(implicit description: Description): Unit = container.succeeded() + + override def beforeTest(description: TestDescription): Unit = { + container match { + case c: TestLifecycleAware => c.beforeTest(description) + case _ => // do nothing + } + } + + override def afterTest(description: TestDescription, throwable: Option[Throwable]): Unit = { + container match { + case c: TestLifecycleAware => c.afterTest(description, throwable) + case _ => // do nothing + } + } + + override def start(): Unit = container.start() + + override def stop(): Unit = container.stop() } object LazyContainer { diff --git a/src/main/scala/com/dimafeng/testcontainers/SeleniumTestContainerSuite.scala b/src/main/scala/com/dimafeng/testcontainers/SeleniumTestContainerSuite.scala index 125d24a3..871b68c6 100644 --- a/src/main/scala/com/dimafeng/testcontainers/SeleniumTestContainerSuite.scala +++ b/src/main/scala/com/dimafeng/testcontainers/SeleniumTestContainerSuite.scala @@ -2,11 +2,14 @@ package com.dimafeng.testcontainers import java.io.File import java.net.URL +import java.util.Optional +import com.dimafeng.testcontainers.lifecycle.TestLifecycleAware import org.openqa.selenium.WebDriver import org.openqa.selenium.remote.{DesiredCapabilities, RemoteWebDriver} import org.scalatest.Suite import org.testcontainers.containers.BrowserWebDriverContainer +import org.testcontainers.lifecycle.TestDescription trait SeleniumTestContainerSuite extends ForEachTestContainer { @@ -23,7 +26,7 @@ trait SeleniumTestContainerSuite extends ForEachTestContainer { class SeleniumContainer(desiredCapabilities: Option[DesiredCapabilities] = None, recordingMode: Option[(BrowserWebDriverContainer.VncRecordingMode, File)] = None) - extends SingleContainer[BrowserWebDriverContainer[_]] { + extends SingleContainer[BrowserWebDriverContainer[_]] with TestLifecycleAware { require(desiredCapabilities.isDefined, "'desiredCapabilities' is required parameter") type OTCContainer = BrowserWebDriverContainer[T] forSome {type T <: BrowserWebDriverContainer[T]} @@ -40,6 +43,14 @@ class SeleniumContainer(desiredCapabilities: Option[DesiredCapabilities] = None, def vncAddress: String = container.getVncAddress def webDriver: RemoteWebDriver = container.getWebDriver + + override def afterTest(description: TestDescription, throwable: Option[Throwable]): Unit = { + val javaThrowable: Optional[Throwable] = throwable match { + case Some(error) => Optional.of(error) + case None => Optional.empty() + } + container.afterTest(description, javaThrowable) + } } object SeleniumContainer { diff --git a/src/main/scala/com/dimafeng/testcontainers/TestContainer.scala b/src/main/scala/com/dimafeng/testcontainers/TestContainer.scala index a738b10c..630be846 100644 --- a/src/main/scala/com/dimafeng/testcontainers/TestContainer.scala +++ b/src/main/scala/com/dimafeng/testcontainers/TestContainer.scala @@ -2,6 +2,9 @@ package com.dimafeng.testcontainers import java.util.function.Consumer +import com.dimafeng.testcontainers.TestContainers.TestContainersSuite +import com.dimafeng.testcontainers.lifecycle.TestLifecycleAware +import org.junit.runner.{Description => JunitDescription} import com.github.dockerjava.api.DockerClient import com.github.dockerjava.api.command.{CreateContainerCmd, InspectContainerResponse} import com.github.dockerjava.api.model.{Bind, Info, VolumesFrom} @@ -11,31 +14,92 @@ import org.testcontainers.containers.output.OutputFrame import org.testcontainers.containers.startupcheck.StartupCheckStrategy import org.testcontainers.containers.traits.LinkableContainer import org.testcontainers.containers.{FailureDetectingExternalResource, Network, TestContainerAccessor, GenericContainer => OTCGenericContainer} +import org.testcontainers.lifecycle.{Startable, TestDescription} import scala.collection.JavaConverters._ import scala.concurrent.{Future, blocking} -trait ForEachTestContainer extends SuiteMixin { - self: Suite => +private[testcontainers] object TestContainers { + + implicit def junit2testContainersDescription(junit: JunitDescription): TestDescription = { + new TestDescription { + override def getTestId: String = junit.getDisplayName + override def getFilesystemFriendlyName: String = s"${junit.getClassName}-${junit.getMethodName}" + } + } + + // Copy-pasted from `org.scalatest.junit.JUnitRunner.createDescription` + def createDescription(suite: Suite): JunitDescription = { + val description = JunitDescription.createSuiteDescription(suite.getClass) + // If we don't add the testNames and nested suites in, we get + // Unrooted Tests show up in Eclipse + for (name <- suite.testNames) { + description.addChild(JunitDescription.createTestDescription(suite.getClass, name)) + } + for (nestedSuite <- suite.nestedSuites) { + description.addChild(createDescription(nestedSuite)) + } + description + } + + trait TestContainersSuite extends SuiteMixin { self: Suite => + + val container: Container + + def afterStart(): Unit = {} + + def beforeStop(): Unit = {} + + private val suiteDescription = createDescription(self) - val container: Container + private[testcontainers] def beforeTest(): Unit = { + container match { + case container: TestLifecycleAware => container.beforeTest(suiteDescription) + case _ => // do nothing + } + } - implicit private val suiteDescription = Description.createSuiteDescription(self.getClass) + private[testcontainers] def afterTest(throwable: Option[Throwable]): Unit = { + container match { + case container: TestLifecycleAware => container.afterTest(suiteDescription, throwable) + case _ => // do nothing + } + } + } +} + +trait ForEachTestContainer extends TestContainersSuite { + self: Suite => abstract protected override def runTest(testName: String, args: Args): Status = { - container.starting() + container.start() + + @volatile var testCalled = false + @volatile var afterTestCalled = false + try { afterStart() + beforeTest() + + testCalled = true val status = super.runTest(testName, args) - status match { - case FailedStatus => container.failed(new RuntimeException(status.toString)) - case _ => container.succeeded() + + afterTestCalled = true + if (!status.succeeds()) { + afterTest(Some(new RuntimeException("Test failed"))) + } else { + afterTest(None) } + status } catch { case e: Throwable => - container.failed(e) + if (testCalled && !afterTestCalled) { + afterTestCalled = true + afterTest(Some(e)) + } + throw e } finally { @@ -43,28 +107,20 @@ trait ForEachTestContainer extends SuiteMixin { beforeStop() } finally { - container.finished() + container.stop() } } } - - def afterStart(): Unit = {} - - def beforeStop(): Unit = {} } -trait ForAllTestContainer extends SuiteMixin { +trait ForAllTestContainer extends TestContainersSuite { self: Suite => - val container: Container - - implicit private val suiteDescription = Description.createSuiteDescription(self.getClass) - abstract override def run(testName: Option[String], args: Args): Status = { if (expectedTestCount(args.filter) == 0) { new CompositeStatus(Set.empty) } else { - container.starting() + container.start() try { afterStart() super.run(testName, args) @@ -73,43 +129,82 @@ trait ForAllTestContainer extends SuiteMixin { beforeStop() } finally { - container.finished() + container.stop() } } } } - def afterStart(): Unit = {} + abstract protected override def runTest(testName: String, args: Args): Status = { + @volatile var testCalled = false + @volatile var afterTestCalled = false + + try { + beforeTest() + + testCalled = true + val status = super.runTest(testName, args) + + afterTestCalled = true + if (!status.succeeds()) { + afterTest(Some(new RuntimeException("Test failed"))) + } else { + afterTest(None) + } - def beforeStop(): Unit = {} + status + } + catch { + case e: Throwable => + if (testCalled && !afterTestCalled) { + afterTestCalled = true + afterTest(Some(e)) + } + + throw e + } + } } -trait Container { - def finished()(implicit description: Description): Unit +trait Container extends Startable { - def failed(e: Throwable)(implicit description: Description): Unit + @deprecated("Use `stop` instead") + def finished()(implicit description: Description): Unit = stop() - def starting()(implicit description: Description): Unit + @deprecated("Use `stop` and/or `TestLifecycleAware.afterTest` instead") + def failed(e: Throwable)(implicit description: Description): Unit = {} - def succeeded()(implicit description: Description): Unit + @deprecated("Use `start` instead") + def starting()(implicit description: Description): Unit = start() + + @deprecated("Use `stop` and/or `TestLifecycleAware.afterTest` instead") + def succeeded()(implicit description: Description): Unit = {} } trait TestContainerProxy[T <: FailureDetectingExternalResource] extends Container { @deprecated("Please use reflective methods from the wrapper and `configure` method for creation") - implicit val container: T + implicit def container: T + @deprecated("Use `stop` instead") override def finished()(implicit description: Description): Unit = TestContainerAccessor.finished(description) + @deprecated("Use `stop` and/or `TestLifecycleAware.afterTest` instead") override def succeeded()(implicit description: Description): Unit = TestContainerAccessor.succeeded(description) + @deprecated("Use `start` instead") override def starting()(implicit description: Description): Unit = TestContainerAccessor.starting(description) + @deprecated("Use `stop` and/or `TestLifecycleAware.afterTest` instead") override def failed(e: Throwable)(implicit description: Description): Unit = TestContainerAccessor.failed(e, description) } abstract class SingleContainer[T <: OTCGenericContainer[_]] extends TestContainerProxy[T] { + override def start(): Unit = container.start() + + override def stop(): Unit = container.stop() + def binds: Seq[Bind] = container.getBinds.asScala.toSeq def command: Seq[String] = container.getCommandParts diff --git a/src/main/scala/com/dimafeng/testcontainers/lifecycle/TestLifecycleAware.scala b/src/main/scala/com/dimafeng/testcontainers/lifecycle/TestLifecycleAware.scala new file mode 100644 index 00000000..4e2da029 --- /dev/null +++ b/src/main/scala/com/dimafeng/testcontainers/lifecycle/TestLifecycleAware.scala @@ -0,0 +1,10 @@ +package com.dimafeng.testcontainers.lifecycle + +import org.testcontainers.lifecycle.TestDescription + +trait TestLifecycleAware { + + def beforeTest(description: TestDescription): Unit = {} + + def afterTest(description: TestDescription, throwable: Option[Throwable]): Unit = {} +} diff --git a/src/main/scala/org/testcontainers/containers/TestContainerAccessor.scala b/src/main/scala/org/testcontainers/containers/TestContainerAccessor.scala index 43e18650..095ffe1f 100644 --- a/src/main/scala/org/testcontainers/containers/TestContainerAccessor.scala +++ b/src/main/scala/org/testcontainers/containers/TestContainerAccessor.scala @@ -2,6 +2,7 @@ package org.testcontainers.containers import org.junit.runner.Description +@deprecated("Should be replaced by lifecycle methods") object TestContainerAccessor { def finished[T <:FailureDetectingExternalResource](description: Description)(implicit container: T): Unit = container.finished(description) diff --git a/src/test/scala/com/dimafeng/testcontainers/ContainerSpec.scala b/src/test/scala/com/dimafeng/testcontainers/ContainerSpec.scala index deda8e9c..cc1ad826 100644 --- a/src/test/scala/com/dimafeng/testcontainers/ContainerSpec.scala +++ b/src/test/scala/com/dimafeng/testcontainers/ContainerSpec.scala @@ -1,12 +1,15 @@ package com.dimafeng.testcontainers +import java.util.Optional + import com.dimafeng.testcontainers.ContainerSpec._ -import org.junit.runner.Description +import com.dimafeng.testcontainers.lifecycle.TestLifecycleAware import org.mockito.ArgumentMatchers._ -import org.mockito.Mockito import org.mockito.Mockito.{times, verify} +import org.mockito.{ArgumentCaptor, ArgumentMatchers, Mockito} import org.scalatest.{Args, FlatSpec, Reporter} import org.testcontainers.containers.{GenericContainer => OTCGenericContainer} +import org.testcontainers.lifecycle.{TestDescription, TestLifecycleAware => JavaTestLifecycleAware} class ContainerSpec extends BaseSpec[ForEachTestContainer] { @@ -17,23 +20,27 @@ class ContainerSpec extends BaseSpec[ForEachTestContainer] { assert(1 == 1) }, new SampleContainer(container)).run(None, Args(mock[Reporter])) - verify(container).starting(any()) - verify(container, times(0)).failed(any(), any()) - verify(container).finished(any()) - verify(container).succeeded(any()) + verify(container).beforeTest(any()) + verify(container).start() + verify(container).afterTest(any(), ArgumentMatchers.eq(Optional.empty())) + verify(container).stop() } it should "call all appropriate methods of the container if assertion fails" in { val container = mock[SampleOTCContainer] + var err: Throwable = null + new TestSpec({ assert(1 == 2) }, new SampleContainer(container)).run(None, Args(mock[Reporter])) - verify(container).starting(any()) - verify(container).failed(any(), any()) - verify(container).finished(any()) - verify(container, times(0)).succeeded(any()) + val captor = ArgumentCaptor.forClass[Optional[Throwable], Optional[Throwable]](classOf[Optional[Throwable]]) + verify(container).beforeTest(any()) + verify(container).start() + verify(container).afterTest(any(), captor.capture()) + assert(captor.getValue.isPresent) + verify(container).stop() } it should "start and stop container only once" in { @@ -43,10 +50,10 @@ class ContainerSpec extends BaseSpec[ForEachTestContainer] { assert(1 == 1) }, new SampleContainer(container)).run(None, Args(mock[Reporter])) - verify(container).starting(any()) - verify(container, times(0)).failed(any(), any()) - verify(container).finished(any()) - verify(container, times(0)).succeeded(any()) + verify(container, times(2)).beforeTest(any()) + verify(container).start() + verify(container, times(2)).afterTest(any(), any()) + verify(container).stop() } it should "call afterStart() and beforeStop()" in { @@ -76,23 +83,24 @@ class ContainerSpec extends BaseSpec[ForEachTestContainer] { intercept[RuntimeException] { specForEach.run(None, Args(mock[Reporter])) } - verify(container).starting(any()) + verify(container, times(0)).beforeTest(any()) + verify(container).start() verify(specForEach).afterStart() - verify(container).failed(any(), any()) + verify(container, times(0)).afterTest(any(), any()) verify(specForEach).beforeStop() - verify(container).finished(any()) - verify(container, times(0)).succeeded(any()) + verify(container).stop() // ForAll val specForAll = Mockito.spy(new MultipleTestsSpecWithFailedAfterStart({}, new SampleContainer(container))) intercept[RuntimeException] { specForAll.run(None, Args(mock[Reporter])) } - verify(container, times(2)).starting(any()) + verify(container, times(0)).beforeTest(any()) + verify(container, times(2)).start() verify(specForAll).afterStart() + verify(container, times(0)).afterTest(any(), any()) verify(specForAll).beforeStop() - verify(container, times(2)).finished(any()) - verify(container, times(0)).succeeded(any()) + verify(container, times(2)).stop() } it should "not start container if all tests are ignored" in { @@ -100,7 +108,7 @@ class ContainerSpec extends BaseSpec[ForEachTestContainer] { val specForAll = Mockito.spy(new TestSpecWithAllIgnored({}, new SampleContainer(container))) specForAll.run(None, Args(mock[Reporter])) - verify(container, Mockito.never()).starting(any()) + verify(container, Mockito.never()).start() } it should "work with `configure` method" in { @@ -166,26 +174,35 @@ object ContainerSpec { } } - class SampleOTCContainer extends OTCGenericContainer { + class SampleOTCContainer extends OTCGenericContainer with JavaTestLifecycleAware { - override def starting(description: Description): Unit = { - println("starting") + override def beforeTest(description: TestDescription): Unit = { + println("beforeTest") } - override def failed(e: Throwable, description: Description): Unit = { - println("failed") + override def afterTest(description: TestDescription, throwable: Optional[Throwable]): Unit = { + println("afterTest") } - override def finished(description: Description): Unit = { - println("finished") + override def start(): Unit = { + println("start") } - override def succeeded(description: Description): Unit = { - println("succeeded") + override def stop(): Unit = { + println("stop") } } - class SampleContainer(sampleOTCContainer: SampleOTCContainer) extends SingleContainer[SampleOTCContainer] { + class SampleContainer(sampleOTCContainer: SampleOTCContainer) + extends SingleContainer[SampleOTCContainer] with TestLifecycleAware { override implicit val container: SampleOTCContainer = sampleOTCContainer + + 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)) + } } } diff --git a/src/test/scala/com/dimafeng/testcontainers/MultipleContainersSpec.scala b/src/test/scala/com/dimafeng/testcontainers/MultipleContainersSpec.scala index 2c220c8b..8d0f6dff 100644 --- a/src/test/scala/com/dimafeng/testcontainers/MultipleContainersSpec.scala +++ b/src/test/scala/com/dimafeng/testcontainers/MultipleContainersSpec.scala @@ -1,12 +1,15 @@ package com.dimafeng.testcontainers +import java.util.Optional + import com.dimafeng.testcontainers.ContainerSpec.{SampleContainer, SampleOTCContainer} -import com.dimafeng.testcontainers.MultipleContainersSpec.{ExampleContainerWithVariable, InitializableContainer, TestSpec} +import com.dimafeng.testcontainers.MultipleContainersSpec.{InitializableContainer, TestSpec} import org.junit.runner.Description +import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito.{times, verify} -import org.scalatest.mockito.MockitoSugar +import org.mockito.Mockito.verify import org.scalatest.{Args, FlatSpec, Reporter} +import org.scalatestplus.mockito.MockitoSugar class MultipleContainersSpec extends BaseSpec[ForEachTestContainer] { it should "call all expected methods of the multiple containers" in { @@ -19,14 +22,15 @@ class MultipleContainersSpec extends BaseSpec[ForEachTestContainer] { assert(1 == 1) }, containers).run(None, Args(mock[Reporter])) - verify(container1).starting(any()) - verify(container1, times(0)).failed(any(), any()) - verify(container1).finished(any()) - verify(container1).succeeded(any()) - verify(container2).starting(any()) - verify(container2, times(0)).failed(any(), any()) - verify(container2).finished(any()) - verify(container2).succeeded(any()) + verify(container1).beforeTest(any()) + verify(container1).start() + verify(container1).afterTest(any(), ArgumentMatchers.eq(Optional.empty())) + verify(container1).stop() + + verify(container2).beforeTest(any()) + verify(container2).start() + verify(container2).afterTest(any(), ArgumentMatchers.eq(Optional.empty())) + verify(container2).stop() } /** @@ -53,15 +57,9 @@ object MultipleContainersSpec { override implicit val container: SampleOTCContainer = mock[SampleOTCContainer] var value: String = _ - override def finished()(implicit description: Description): Unit = () - - override def succeeded()(implicit description: Description): Unit = () - - override def starting()(implicit description: Description): Unit = { + override def start(): Unit = { value = valueToBeSetAfterStart } - - override def failed(e: Throwable)(implicit description: Description): Unit = () } class ExampleContainerWithVariable(val variable: String) extends SingleContainer[SampleOTCContainer] with MockitoSugar { diff --git a/version.sbt b/version.sbt index 57258bf1..8f9b28b3 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "0.26.1-SNAPSHOT" +version in ThisBuild := "0.27.0-SNAPSHOT"