diff --git a/README.md b/README.md index 7bda4780..96917bfa 100644 --- a/README.md +++ b/README.md @@ -254,8 +254,137 @@ class MysqlSpec extends FlatSpec with ForAllTestContainer { } ``` +## New API + +Starting from 0.34.0 version testcontainers-scala provides the new API. +The main motivation points are in the [pull request](https://github.com/testcontainers/testcontainers-scala/pull/78). + +**This API is experimental and may change!** + +### `Container` and `ContainerDef` + +Docker containers are represented through the two different entities: +1. `ContainerDef` — it's container definition. `ContainerDef` describes, how to build a container. + You can think about it like about a container constructor, or dockerfile description. + Usually, `ContainerDef` receives some parameters. + `ContainerDef` has a `start()` method. It returns a started `Container`. +2. `Container` — it's a started container. You can interact with it through its methods. + For example, in the case of `MySQLContainer` you can get it's JDBC URL with `jdbcUrl` method. + `Container` is the main entity for using inside tests. + +### Scalatest usage + +You can use one of the four traits: +1. `TestContainerForAll` — will start a single container before all tests and stop after all tests. +2. `TestContainerForEach` — will start a single container before each test and stop after each test. +3. `TestContainersForAll` — will start multiple containers before all tests and stop after all tests. +4. `TestContainersForEach` — will start multiple containers before each test and stop after each test. + + +#### Single container in tests + +If you want to use a single container in your test: +```scala +class MysqlSpec extends FlatSpec with TestContainerForAll { + + // You need to override `containerDef` with needed container definition + override val containerDef = MySQLContainer.Def() + + // To use containers in tests you need to use `withContainers` function + it should "test" in withContainers { mysqlContainer => + // Inside your test body you can do with your container whatever you want to + assert(mysqlContainer.jdbcUrl.nonEmpty) + } +} +``` + +Usage of `TestContainerForEach` is not different from the example above. + +#### Multiple containers in tests + +If you want to use multiple containers in your test: +```scala +class ExampleSpec extends FlatSpec with TestContainersForAll { + + // First of all, you need to declare, which containers you want to use + override type Containers = MySQLContainer and PostgreSQLContainer + + // After that, you need to describe, how you want to start them, + // In this method you can use any intermediate logic. + // You can pass parameters between containers, for example. + override def startContainers(): Containers = { + val container1 = MySQLContainer.Def().start() + val container2 = PostgreSQLContainer.Def().start() + container1 and container2 + } + + // `withContainers` function supports multiple containers: + it should "test" in withContainers { case mysqlContainer and pgContainer => + // Inside your test body you can do with your containers whatever you want to + assert(mysqlContainer.jdbcUrl.nonEmpty && pgContainer.jdbcUrl.nonEmpty) + } + +} +``` + +Usage of `TestContainersForEach` is not different from the example above. + +### `GenericContainer` usage + +To create a custom container, which is not built-in in the library, you need to use `GenericContainer`. + +For example, you want to create a custom nginx container: +```scala +class NginxContainer(port: Int, underlying: GenericContainer) extends GenericContainer(underlying) { + // you can add any methods or fields inside your container's body + def rootUrl: String = s"http://$containerIpAddress:${mappedPort(port)}/" +} +object NginxContainer { + + // In the container definition you need to describe, how your container will be constructed: + case class Def(port: Int) extends GenericContainer.Def[NginxContainer]( + new NginxContainer(port, GenericContainer( + dockerImage = "nginx:latest", + exposedPorts = Seq(port), + waitStrategy = Wait.forHttp("/") + )) + ) +} +``` + +### Migration from the classic API + +1. If you have custom containers created with the `GenericContainer`, add `ContainerDef` in the companion like this: + ```scala + object MyCustomContainer { + case class Def(/*constructor params here*/) extends GenericContainer.Def[MyCustomContainer]( + new MyCustomContainer(/*constructor params here*/) + ) + } + ``` +2. If you are using `ForEachTestContainer`: + 1. If your test contains only one container, replace `ForEachTestContainer` with `TestContainerForEach` + 2. If your test contains multiple containers, replace `ForEachTestContainer` with `TestContainersForEach` +3. If you are using `ForAllTestContainer`: + 1. If your test contains only one container, replace `ForAllTestContainer` with `TestContainerForAll` + 2. If your test contains multiple containers, replace `ForAllTestContainer` with `TestContainersForAll` +4. Fix all compilation errors using compiler messages and examples above. + +If you have any questions or difficulties feel free to ask it in our [slack channel](https://testcontainers.slack.com/messages/CAFK4GL85). + ## Release notes +* **0.34.0** + * Added new, experimental API and DSL. + The main motivation points are in the [pull request](https://github.com/testcontainers/testcontainers-scala/pull/78). + Old API remains the same, so all your old code will continue to work. + We will wait for the user's feedback about the new API. + If it will be positive, eventually this API may replace the current API. + You can find more information about the new API above. + +* **0.33.0** + * TODO + * **0.32.0** * TestContainers -> `1.12.1` * SBT -> `1.3.0` diff --git a/core/src/main/scala/com/dimafeng/testcontainers/Container.scala b/core/src/main/scala/com/dimafeng/testcontainers/Container.scala index 8c00e257..b2065ccd 100644 --- a/core/src/main/scala/com/dimafeng/testcontainers/Container.scala +++ b/core/src/main/scala/com/dimafeng/testcontainers/Container.scala @@ -2,6 +2,7 @@ package com.dimafeng.testcontainers import java.util.function.Consumer +import com.dimafeng.testcontainers.lifecycle.Stoppable import com.github.dockerjava.api.DockerClient import com.github.dockerjava.api.command.{CreateContainerCmd, InspectContainerResponse} import com.github.dockerjava.api.model.{Bind, Info, VolumesFrom} @@ -9,12 +10,13 @@ import org.junit.runner.Description 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.containers.{FailureDetectingExternalResource, Network, TestContainerAccessor, GenericContainer => JavaGenericContainer} import org.testcontainers.lifecycle.Startable import scala.collection.JavaConverters._ import scala.concurrent.{Future, blocking} +@deprecated("For internal usage only. Will be deleted.") trait TestContainerProxy[T <: FailureDetectingExternalResource] extends Container { @deprecated("Please use reflective methods from the wrapper and `configure` method for creation") @@ -33,7 +35,9 @@ trait TestContainerProxy[T <: FailureDetectingExternalResource] extends Containe override def failed(e: Throwable)(implicit description: Description): Unit = TestContainerAccessor.failed(e, description) } -abstract class SingleContainer[T <: OTCGenericContainer[_]] extends TestContainerProxy[T] { +abstract class SingleContainer[T <: JavaGenericContainer[_]] extends TestContainerProxy[T] { + + def underlyingUnsafeContainer: T = container override def start(): Unit = container.start() @@ -102,7 +106,7 @@ abstract class SingleContainer[T <: OTCGenericContainer[_]] extends TestContaine } } -trait Container extends Startable { +trait Container extends Startable with Stoppable { @deprecated("Use `stop` instead") def finished()(implicit description: Description): Unit = stop() diff --git a/core/src/main/scala/com/dimafeng/testcontainers/ContainerDef.scala b/core/src/main/scala/com/dimafeng/testcontainers/ContainerDef.scala new file mode 100644 index 00000000..4514edc6 --- /dev/null +++ b/core/src/main/scala/com/dimafeng/testcontainers/ContainerDef.scala @@ -0,0 +1,17 @@ +package com.dimafeng.testcontainers + +import com.dimafeng.testcontainers.lifecycle.Stoppable +import org.testcontainers.lifecycle.Startable + +trait ContainerDef { + + type Container <: Startable with Stoppable + + protected def createContainer(): Container + + def start(): Container = { + val container = createContainer() + container.start() + container + } +} diff --git a/core/src/main/scala/com/dimafeng/testcontainers/DockerComposeContainer.scala b/core/src/main/scala/com/dimafeng/testcontainers/DockerComposeContainer.scala index 9172279f..8f61bc7f 100644 --- a/core/src/main/scala/com/dimafeng/testcontainers/DockerComposeContainer.scala +++ b/core/src/main/scala/com/dimafeng/testcontainers/DockerComposeContainer.scala @@ -7,7 +7,7 @@ import java.util.function.Consumer import com.dimafeng.testcontainers.DockerComposeContainer.ComposeFile import org.testcontainers.containers.output.OutputFrame import org.testcontainers.containers.wait.strategy.{Wait, WaitStrategy} -import org.testcontainers.containers.{DockerComposeContainer => OTCDockerComposeContainer} +import org.testcontainers.containers.{DockerComposeContainer => JavaDockerComposeContainer} import org.testcontainers.utility.Base58 import scala.collection.JavaConverters._ @@ -73,10 +73,10 @@ class DockerComposeContainer(composeFiles: ComposeFile, env: Map[String, String] = Map.empty, tailChildContainers: Boolean = false, logConsumers: Seq[ServiceLogConsumer] = Seq.empty) - extends TestContainerProxy[OTCDockerComposeContainer[_]] { + extends TestContainerProxy[JavaDockerComposeContainer[_]] { - override val container: OTCDockerComposeContainer[_] = { - val container: OTCDockerComposeContainer[_] = new OTCDockerComposeContainer(identifier, composeFiles match { + override val container: JavaDockerComposeContainer[_] = { + val container: JavaDockerComposeContainer[_] = new JavaDockerComposeContainer(identifier, composeFiles match { case ComposeFile(Left(f)) => util.Arrays.asList(f) case ComposeFile(Right(files)) => files.asJava }) diff --git a/core/src/main/scala/com/dimafeng/testcontainers/FixedHostPortGenericContainer.scala b/core/src/main/scala/com/dimafeng/testcontainers/FixedHostPortGenericContainer.scala index 686418f7..00700bad 100644 --- a/core/src/main/scala/com/dimafeng/testcontainers/FixedHostPortGenericContainer.scala +++ b/core/src/main/scala/com/dimafeng/testcontainers/FixedHostPortGenericContainer.scala @@ -1,7 +1,7 @@ package com.dimafeng.testcontainers import org.testcontainers.containers.wait.strategy.WaitStrategy -import org.testcontainers.containers.{BindMode, FixedHostPortGenericContainer => OTCFixedHostPortGenericContainer} +import org.testcontainers.containers.{BindMode, FixedHostPortGenericContainer => JavaFixedHostPortGenericContainer} class FixedHostPortGenericContainer(imageName: String, exposedPorts: Seq[Int] = Seq(), @@ -11,9 +11,9 @@ class FixedHostPortGenericContainer(imageName: String, waitStrategy: Option[WaitStrategy] = None, exposedHostPort: Int, exposedContainerPort: Int - ) extends SingleContainer[OTCFixedHostPortGenericContainer[_]] { + ) extends SingleContainer[JavaFixedHostPortGenericContainer[_]] { - override implicit val container: OTCFixedHostPortGenericContainer[_] = new OTCFixedHostPortGenericContainer(imageName) + override implicit val container: JavaFixedHostPortGenericContainer[_] = new JavaFixedHostPortGenericContainer(imageName) if (exposedPorts.nonEmpty) { container.withExposedPorts(exposedPorts.map(int2Integer): _*) diff --git a/core/src/main/scala/com/dimafeng/testcontainers/GenericContainer.scala b/core/src/main/scala/com/dimafeng/testcontainers/GenericContainer.scala index fb45f150..6c85c47e 100644 --- a/core/src/main/scala/com/dimafeng/testcontainers/GenericContainer.scala +++ b/core/src/main/scala/com/dimafeng/testcontainers/GenericContainer.scala @@ -4,30 +4,41 @@ import java.util.concurrent.Future import com.dimafeng.testcontainers.GenericContainer.DockerImage import org.testcontainers.containers.wait.strategy.WaitStrategy -import org.testcontainers.containers.{BindMode, GenericContainer => OTCGenericContainer} - -class GenericContainer(dockerImage: DockerImage, - exposedPorts: Seq[Int] = Seq(), - env: Map[String, String] = Map(), - command: Seq[String] = Seq(), - classpathResourceMapping: Seq[(String, String, BindMode)] = Seq(), - waitStrategy: Option[WaitStrategy] = None - ) extends SingleContainer[OTCGenericContainer[_]] { - - override implicit val container: OTCGenericContainer[_] = dockerImage match { - case DockerImage(Left(imageFromDockerfile)) => new OTCGenericContainer(imageFromDockerfile) - case DockerImage(Right(imageName)) => new OTCGenericContainer(imageName) - } +import org.testcontainers.containers.{BindMode, GenericContainer => JavaGenericContainer} - if (exposedPorts.nonEmpty) { - container.withExposedPorts(exposedPorts.map(int2Integer): _*) - } - env.foreach(Function.tupled(container.withEnv)) - if (command.nonEmpty) { - container.withCommand(command: _*) - } - classpathResourceMapping.foreach(Function.tupled(container.withClasspathResourceMapping)) - waitStrategy.foreach(container.waitingFor) +class GenericContainer( + override val underlyingUnsafeContainer: JavaGenericContainer[_] +) extends SingleContainer[JavaGenericContainer[_]] { + + override implicit val container: JavaGenericContainer[_] = underlyingUnsafeContainer + + def this( + dockerImage: DockerImage, + exposedPorts: Seq[Int] = Seq(), + env: Map[String, String] = Map(), + command: Seq[String] = Seq(), + classpathResourceMapping: Seq[(String, String, BindMode)] = Seq(), + waitStrategy: Option[WaitStrategy] = None + ) = this({ + val underlying: JavaGenericContainer[_] = dockerImage match { + case DockerImage(Left(imageFromDockerfile)) => new JavaGenericContainer(imageFromDockerfile) + case DockerImage(Right(imageName)) => new JavaGenericContainer(imageName) + } + + if (exposedPorts.nonEmpty) { + underlying.withExposedPorts(exposedPorts.map(int2Integer): _*) + } + env.foreach(Function.tupled(underlying.withEnv)) + if (command.nonEmpty) { + underlying.withCommand(command: _*) + } + classpathResourceMapping.foreach(Function.tupled(underlying.withClasspathResourceMapping)) + waitStrategy.foreach(underlying.waitingFor) + + underlying + }) + + def this(genericContainer: GenericContainer) = this(genericContainer.underlyingUnsafeContainer) } object GenericContainer { @@ -48,4 +59,9 @@ object GenericContainer { classpathResourceMapping: Seq[(String, String, BindMode)] = Seq(), waitStrategy: WaitStrategy = null): GenericContainer = new GenericContainer(dockerImage, exposedPorts, env, command, classpathResourceMapping, Option(waitStrategy)) + + abstract class Def[C <: GenericContainer](init: => C) extends ContainerDef { + override type Container = C + protected def createContainer(): C = init + } } diff --git a/core/src/main/scala/com/dimafeng/testcontainers/lifecycle/Stoppable.scala b/core/src/main/scala/com/dimafeng/testcontainers/lifecycle/Stoppable.scala new file mode 100644 index 00000000..3ac79f5b --- /dev/null +++ b/core/src/main/scala/com/dimafeng/testcontainers/lifecycle/Stoppable.scala @@ -0,0 +1,46 @@ +package com.dimafeng.testcontainers.lifecycle + +trait Stoppable extends AutoCloseable with Andable { + + def stop(): Unit + + override def close(): Unit = stop() +} + +/** + * Gives you a possibility to write `container1 and container2`. + * + * Used for tests DSL. + */ +sealed trait Andable { + + def stop(): Unit + + def foreach(f: Stoppable => Unit): Unit = { + this match { + case and(head, tail) => + head.foreach(f) + tail.foreach(f) + + case stoppable: Stoppable => + f(stoppable) + } + } + +} +final case class and[A1 <: Andable, A2 <: Andable](head: A1, tail: A2) extends Andable { + + /** + * Stopping all Andable elements in the reverse order + */ + override def stop(): Unit = { + tail.stop() + head.stop() + } +} + +object Andable { + implicit class AndableOps[A <: Andable](val self: A) extends AnyVal { + def and[A2 <: Andable](that: A2): A and A2 = com.dimafeng.testcontainers.lifecycle.and(self, that) + } +} diff --git a/core/src/test/scala/com/dimafeng/testcontainers/BaseSpec.scala b/core/src/test/scala/com/dimafeng/testcontainers/BaseSpec.scala index ac1729c1..416407af 100644 --- a/core/src/test/scala/com/dimafeng/testcontainers/BaseSpec.scala +++ b/core/src/test/scala/com/dimafeng/testcontainers/BaseSpec.scala @@ -2,7 +2,7 @@ package com.dimafeng.testcontainers import org.mockito.MockitoAnnotations import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, FlatSpec, Matchers} -import org.scalatest.mockito.MockitoSugar +import org.scalatestplus.mockito.MockitoSugar abstract class BaseSpec[T: Manifest] extends FlatSpec with Matchers with MockitoSugar with BeforeAndAfterEach with BeforeAndAfterAll { diff --git a/core/src/test/scala/com/dimafeng/testcontainers/lifecycle/AndableTest.scala b/core/src/test/scala/com/dimafeng/testcontainers/lifecycle/AndableTest.scala new file mode 100644 index 00000000..debf07f0 --- /dev/null +++ b/core/src/test/scala/com/dimafeng/testcontainers/lifecycle/AndableTest.scala @@ -0,0 +1,44 @@ +package com.dimafeng.testcontainers.lifecycle + +import org.scalatest.FreeSpec + +import scala.collection.mutable + +class AndableTest extends FreeSpec { + + case class Cont1(i: Int, buffer: mutable.Buffer[Int] = mutable.Buffer.empty[Int]) extends Stoppable { + override def stop(): Unit = buffer.append(i) + } + + case class Cont2(i: Int, buffer: mutable.Buffer[Int] = mutable.Buffer.empty[Int]) extends Stoppable { + override def stop(): Unit = buffer.append(i) + } + + "Andable" - { + "foreach" - { + "should iterate Andable in a correct order" in { + val andable = Cont1(1) and Cont1(2) and Cont2(3) and Cont2(4) + + val ixs = mutable.Buffer.empty[Int] + andable.foreach { + case Cont1(i, _) => ixs.append(i) + case Cont2(i, _) => ixs.append(i) + } + + assert(ixs.toSeq === Seq(1,2,3,4)) + } + } + + "stop" - { + "should stop all Andable in a reverse order" in { + val ixs = mutable.Buffer.empty[Int] + + val andable = Cont1(1, ixs) and Cont1(2, ixs) and Cont2(3, ixs) and Cont2(4, ixs) + andable.stop() + + assert(ixs.toSeq === Seq(4,3,2,1)) + } + } + } + +} diff --git a/modules/cassandra/src/main/scala/com/dimafeng/testcontainers/CassandraContainer.scala b/modules/cassandra/src/main/scala/com/dimafeng/testcontainers/CassandraContainer.scala index 1c9d8025..484951f4 100644 --- a/modules/cassandra/src/main/scala/com/dimafeng/testcontainers/CassandraContainer.scala +++ b/modules/cassandra/src/main/scala/com/dimafeng/testcontainers/CassandraContainer.scala @@ -1,17 +1,17 @@ package com.dimafeng.testcontainers -import org.testcontainers.containers.{CassandraContainer => OTCCassandraContainer, GenericContainer => OTCGenericContainer} +import org.testcontainers.containers.{CassandraContainer => JavaCassandraContainer} class CassandraContainer(dockerImageNameOverride: Option[String] = None, configurationOverride: Option[String] = None, initScript: Option[String] = None, - jmxReporting: Boolean = false) extends SingleContainer[OTCCassandraContainer[_]] { + jmxReporting: Boolean = false) extends SingleContainer[JavaCassandraContainer[_]] { - val cassandraContainer: OTCCassandraContainer[_] = { + val cassandraContainer: JavaCassandraContainer[_] = { if (dockerImageNameOverride.isEmpty) { - new OTCCassandraContainer() + new JavaCassandraContainer() } else { - new OTCCassandraContainer(dockerImageNameOverride.get) + new JavaCassandraContainer(dockerImageNameOverride.get) } } @@ -19,12 +19,14 @@ class CassandraContainer(dockerImageNameOverride: Option[String] = None, if (initScript.isDefined) cassandraContainer.withInitScript(initScript.get) if (jmxReporting) cassandraContainer.withJmxReporting(jmxReporting) - override val container: OTCCassandraContainer[_] = cassandraContainer + override val container: JavaCassandraContainer[_] = cassandraContainer } object CassandraContainer { + val defaultDockerImageName = "cassandra:3.11.2" + def apply(dockerImageNameOverride: String = null, configurationOverride: String = null, initScript: String = null, @@ -35,4 +37,23 @@ object CassandraContainer { jmxReporting ) + case class Def( + dockerImageName: String = defaultDockerImageName, + configurationOverride: Option[String] = None, + initScript: Option[String] = None, + jmxReporting: Boolean = false + ) extends ContainerDef { + + override type Container = CassandraContainer + + override def createContainer(): CassandraContainer = { + new CassandraContainer( + dockerImageNameOverride = Some(dockerImageName), + configurationOverride = configurationOverride, + initScript = initScript, + jmxReporting = jmxReporting + ) + } + } + } diff --git a/modules/kafka/src/main/scala/com/dimafeng/testcontainers/KafkaContainer.scala b/modules/kafka/src/main/scala/com/dimafeng/testcontainers/KafkaContainer.scala index 092a6f83..a065804d 100644 --- a/modules/kafka/src/main/scala/com/dimafeng/testcontainers/KafkaContainer.scala +++ b/modules/kafka/src/main/scala/com/dimafeng/testcontainers/KafkaContainer.scala @@ -1,16 +1,16 @@ package com.dimafeng.testcontainers -import org.testcontainers.containers.{KafkaContainer => OTCKafkaContainer} +import org.testcontainers.containers.{KafkaContainer => JavaKafkaContainer} class KafkaContainer(confluentPlatformVersion: Option[String] = None, - externalZookeeper: Option[String] = None) extends SingleContainer[OTCKafkaContainer] { + externalZookeeper: Option[String] = None) extends SingleContainer[JavaKafkaContainer] { @deprecated("Please use reflective methods of the scala container or `configure` method") - val kafkaContainer: OTCKafkaContainer = { + val kafkaContainer: JavaKafkaContainer = { if (confluentPlatformVersion.isEmpty) { - new OTCKafkaContainer() + new JavaKafkaContainer() } else { - new OTCKafkaContainer(confluentPlatformVersion.get) + new JavaKafkaContainer(confluentPlatformVersion.get) } } @@ -20,14 +20,32 @@ class KafkaContainer(confluentPlatformVersion: Option[String] = None, kafkaContainer.withExternalZookeeper(externalZookeeper.get) } - override val container: OTCKafkaContainer = kafkaContainer + override val container: JavaKafkaContainer = kafkaContainer def bootstrapServers: String = container.getBootstrapServers } object KafkaContainer { + + val defaultTag = "5.2.1" + def apply(confluentPlatformVersion: String = null, externalZookeeper: String = null): KafkaContainer = { new KafkaContainer(Option(confluentPlatformVersion), Option(externalZookeeper)) } + + case class Def( + confluentPlatformVersion: String = defaultTag, + externalZookeeper: Option[String] = None + ) extends ContainerDef { + + override type Container = KafkaContainer + + override def createContainer(): KafkaContainer = { + new KafkaContainer( + confluentPlatformVersion = Some(confluentPlatformVersion), + externalZookeeper = externalZookeeper + ) + } + } } diff --git a/modules/mysql/src/main/scala/com/dimafeng/testcontainers/MySQLContainer.scala b/modules/mysql/src/main/scala/com/dimafeng/testcontainers/MySQLContainer.scala index 4b451daa..7ff0055b 100644 --- a/modules/mysql/src/main/scala/com/dimafeng/testcontainers/MySQLContainer.scala +++ b/modules/mysql/src/main/scala/com/dimafeng/testcontainers/MySQLContainer.scala @@ -1,19 +1,17 @@ package com.dimafeng.testcontainers -import com.dimafeng.testcontainers.MySQLContainer.DEFAULT_MYSQL_VERSION -import org.testcontainers.containers.{MySQLContainer => OTCMySQLContainer} - +import org.testcontainers.containers.{MySQLContainer => JavaMySQLContainer} class MySQLContainer(configurationOverride: Option[String] = None, mysqlImageVersion: Option[String] = None, databaseName: Option[String] = None, mysqlUsername: Option[String] = None, mysqlPassword: Option[String] = None) - extends SingleContainer[OTCMySQLContainer[_]] { + extends SingleContainer[JavaMySQLContainer[_]] { - override val container: OTCMySQLContainer[_] = mysqlImageVersion - .map(new OTCMySQLContainer(_)) - .getOrElse(new OTCMySQLContainer(DEFAULT_MYSQL_VERSION)) + override val container: JavaMySQLContainer[_] = mysqlImageVersion + .map(new JavaMySQLContainer(_)) + .getOrElse(new JavaMySQLContainer(MySQLContainer.DEFAULT_MYSQL_VERSION)) databaseName.map(container.withDatabaseName) mysqlUsername.map(container.withUsername) @@ -34,7 +32,13 @@ class MySQLContainer(configurationOverride: Option[String] = None, } object MySQLContainer { - val DEFAULT_MYSQL_VERSION = "mysql:5" + + val defaultDockerImageName = s"${JavaMySQLContainer.IMAGE}:${JavaMySQLContainer.DEFAULT_TAG}" + val defaultDatabaseName = "test" + val defaultUsername = "test" + val defaultPassword = "test" + + val DEFAULT_MYSQL_VERSION = defaultDockerImageName def apply(configurationOverride: String = null, mysqlImageVersion: String = null, @@ -47,4 +51,25 @@ object MySQLContainer { Option(username), Option(password)) + case class Def( + dockerImageName: String = defaultDockerImageName, + databaseName: String = defaultDatabaseName, + username: String = defaultUsername, + password: String = defaultPassword, + configurationOverride: Option[String] = None + ) extends ContainerDef { + + override type Container = MySQLContainer + + override def createContainer(): MySQLContainer = { + new MySQLContainer( + mysqlImageVersion = Some(dockerImageName), + databaseName = Some(databaseName), + mysqlUsername = Some(username), + mysqlPassword = Some(password), + configurationOverride = configurationOverride + ) + } + } + } diff --git a/modules/postgres/src/main/scala/com/dimafeng/testcontainers/PostgreSQLContainer.scala b/modules/postgres/src/main/scala/com/dimafeng/testcontainers/PostgreSQLContainer.scala index 727c3a0c..5462de34 100644 --- a/modules/postgres/src/main/scala/com/dimafeng/testcontainers/PostgreSQLContainer.scala +++ b/modules/postgres/src/main/scala/com/dimafeng/testcontainers/PostgreSQLContainer.scala @@ -1,20 +1,20 @@ package com.dimafeng.testcontainers -import org.testcontainers.containers.{PostgreSQLContainer => OTCPostgreSQLContainer} +import org.testcontainers.containers.{PostgreSQLContainer => JavaPostgreSQLContainer} class PostgreSQLContainer(dockerImageNameOverride: Option[String] = None, databaseName: Option[String] = None, pgUsername: Option[String] = None, pgPassword: Option[String] = None, - mountPostgresDataToTmpfs: Boolean = false) extends SingleContainer[OTCPostgreSQLContainer[_]] { + mountPostgresDataToTmpfs: Boolean = false) extends SingleContainer[JavaPostgreSQLContainer[_]] { - override val container: OTCPostgreSQLContainer[_] = dockerImageNameOverride match { + override val container: JavaPostgreSQLContainer[_] = dockerImageNameOverride match { case Some(imageNameOverride) => - new OTCPostgreSQLContainer(imageNameOverride) + new JavaPostgreSQLContainer(imageNameOverride) case None => - new OTCPostgreSQLContainer() + new JavaPostgreSQLContainer() } databaseName.map(container.withDatabaseName) @@ -42,6 +42,12 @@ class PostgreSQLContainer(dockerImageNameOverride: Option[String] = None, } object PostgreSQLContainer { + + val defaultDockerImageName = s"${JavaPostgreSQLContainer.IMAGE}:${JavaPostgreSQLContainer.DEFAULT_TAG}" + val defaultDatabaseName = "test" + val defaultUsername = "test" + val defaultPassword = "test" + def apply(dockerImageNameOverride: String = null, databaseName: String = null, username: String = null, @@ -55,4 +61,25 @@ object PostgreSQLContainer { Option(password), mountPostgresDataToTmpfs ) + + case class Def( + dockerImageName: String = defaultDockerImageName, + databaseName: String = defaultDatabaseName, + username: String = defaultUsername, + password: String = defaultPassword, + mountPostgresDataToTmpfs: Boolean = false + ) extends ContainerDef { + + override type Container = PostgreSQLContainer + + override def createContainer(): PostgreSQLContainer = { + new PostgreSQLContainer( + dockerImageNameOverride = Some(dockerImageName), + databaseName = Some(databaseName), + pgUsername = Some(username), + pgPassword = Some(password), + mountPostgresDataToTmpfs = mountPostgresDataToTmpfs + ) + } + } } diff --git a/modules/vault/src/main/scala/com/dimafeng/testcontainers/VaultContainer.scala b/modules/vault/src/main/scala/com/dimafeng/testcontainers/VaultContainer.scala index b411e6cf..b1f74587 100644 --- a/modules/vault/src/main/scala/com/dimafeng/testcontainers/VaultContainer.scala +++ b/modules/vault/src/main/scala/com/dimafeng/testcontainers/VaultContainer.scala @@ -1,27 +1,29 @@ package com.dimafeng.testcontainers; -import org.testcontainers.vault.{VaultContainer => OTCVaultContainer} +import org.testcontainers.vault.{VaultContainer => JavaVaultContainer} class VaultContainer(dockerImageNameOverride: Option[String] = None, vaultToken: Option[String] = None, - vaultPort: Option[Int]) extends SingleContainer[OTCVaultContainer[_]] { + vaultPort: Option[Int]) extends SingleContainer[JavaVaultContainer[_]] { - val vaultContainer: OTCVaultContainer[Nothing] = { + val vaultContainer: JavaVaultContainer[Nothing] = { if (dockerImageNameOverride.isEmpty) { - new OTCVaultContainer() + new JavaVaultContainer() } else { - new OTCVaultContainer(dockerImageNameOverride.get) + new JavaVaultContainer(dockerImageNameOverride.get) } } if (vaultToken.isDefined) vaultContainer.withVaultToken(vaultToken.get) if (vaultPort.isDefined) vaultContainer.withVaultPort(vaultPort.get) - override val container: OTCVaultContainer[_] = vaultContainer + override val container: JavaVaultContainer[_] = vaultContainer } object VaultContainer { + val defaultDockerImageName = "vault:0.7.0" + def apply(dockerImageNameOverride: String = null, vaultToken: String = null, vaultPort: Option[Int] = None): VaultContainer = new VaultContainer( @@ -30,4 +32,21 @@ object VaultContainer { vaultPort ) + case class Def( + dockerImageName: String = defaultDockerImageName, + vaultToken: Option[String] = None, + vaultPort: Option[Int] = None + ) extends ContainerDef { + + override type Container = VaultContainer + + override def createContainer(): VaultContainer = { + new VaultContainer( + dockerImageNameOverride = Some(dockerImageName), + vaultToken = vaultToken, + vaultPort = vaultPort + ) + } + } + } diff --git a/test-framework/scalatest-selenium/src/test/scala/com/dimafeng/testcontainers/integration/SeleniumSpec.scala b/test-framework/scalatest-selenium/src/test/scala/com/dimafeng/testcontainers/integration/SeleniumSpec.scala index 70d13581..f56a125c 100644 --- a/test-framework/scalatest-selenium/src/test/scala/com/dimafeng/testcontainers/integration/SeleniumSpec.scala +++ b/test-framework/scalatest-selenium/src/test/scala/com/dimafeng/testcontainers/integration/SeleniumSpec.scala @@ -3,7 +3,7 @@ package com.dimafeng.testcontainers.integration import com.dimafeng.testcontainers.SeleniumTestContainerSuite import org.openqa.selenium.remote.DesiredCapabilities import org.scalatest.FlatSpec -import org.scalatest.selenium.WebBrowser +import org.scalatestplus.selenium.WebBrowser class SeleniumSpec extends FlatSpec with SeleniumTestContainerSuite with WebBrowser { override def desiredCapabilities = DesiredCapabilities.chrome() diff --git a/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainerForAll.scala b/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainerForAll.scala new file mode 100644 index 00000000..31b1939c --- /dev/null +++ b/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainerForAll.scala @@ -0,0 +1,33 @@ +package com.dimafeng.testcontainers.scalatest + +import com.dimafeng.testcontainers.ContainerDef +import org.scalatest.Suite + +/** + * Starts a single container before all tests and stop it after all tests + * + * Example: + * {{{ + * class MysqlSpec extends FlatSpec with TestContainerForAll { + * + * // You need to override `containerDef` with needed container definition + * override val containerDef = MySQLContainer.Def() + * + * // To use containers in tests you need to use `withContainers` function + * it should "test" in withContainers { mysqlContainer => + * // Inside your test body you can do with your container whatever you want to + * assert(mysqlContainer.jdbcUrl.nonEmpty) + * } + * } + * }}} + */ +trait TestContainerForAll extends TestContainersForAll { self: Suite => + + val containerDef: ContainerDef + + final override type Containers = containerDef.Container + + override def startContainers(): containerDef.Container = { + containerDef.start() + } +} diff --git a/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainerForEach.scala b/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainerForEach.scala new file mode 100644 index 00000000..fed81e13 --- /dev/null +++ b/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainerForEach.scala @@ -0,0 +1,33 @@ +package com.dimafeng.testcontainers.scalatest + +import com.dimafeng.testcontainers.ContainerDef +import org.scalatest.Suite + +/** + * Starts a single container before each test and stop it after each test + * + * Example: + * {{{ + * class MysqlSpec extends FlatSpec with TestContainerForEach { + * + * // You need to override `containerDef` with needed container definition + * override val containerDef = MySQLContainer.Def() + * + * // To use containers in tests you need to use `withContainers` function + * it should "test" in withContainers { mysqlContainer => + * // Inside your test body you can do with your container whatever you want to + * assert(mysqlContainer.jdbcUrl.nonEmpty) + * } + * } + * }}} + */ +trait TestContainerForEach extends TestContainersForEach { self: Suite => + + val containerDef: ContainerDef + + final override type Containers = containerDef.Container + + override def startContainers(): containerDef.Container = { + containerDef.start() + } +} diff --git a/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainersForAll.scala b/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainersForAll.scala new file mode 100644 index 00000000..a8f15fb8 --- /dev/null +++ b/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainersForAll.scala @@ -0,0 +1,89 @@ +package com.dimafeng.testcontainers.scalatest + +import org.scalatest.{Args, CompositeStatus, Status, Suite} + +/** + * Starts containers before all tests and stop then after all tests + * + * Example: + * {{{ + * class ExampleSpec extends FlatSpec with TestContainersForAll { + * + * // First of all, you need to declare, which containers you want to use + * override type Containers = MySQLContainer and PostgreSQLContainer + * + * // After that, you need to describe, how you want to start them, + * // In this method you can use any intermediate logic. + * // You can pass parameters between containers, for example. + * override def startContainers(): Containers = { + * val container1 = MySQLContainer.Def().start() + * val container2 = PostgreSQLContainer.Def().start() + * container1 and container2 + * } + * + * // `withContainers` function supports multiple containers: + * it should "test" in withContainers { case mysqlContainer and pgContainer => + * // Inside your test body you can do with your containers whatever you want to + * assert(mysqlContainer.jdbcUrl.nonEmpty && pgContainer.jdbcUrl.nonEmpty) + * } + * + * } + * }}} + */ +trait TestContainersForAll extends TestContainersSuite { self: Suite => + + abstract override def run(testName: Option[String], args: Args): Status = { + if (expectedTestCount(args.filter) == 0) { + new CompositeStatus(Set.empty) + } else { + startedContainers = Some(startContainers()) + try { + afterStart() + super.run(testName, args) + } finally { + try { + beforeStop() + } + finally { + try { + startedContainers.foreach(_.stop()) + } + finally { + startedContainers = None + } + } + } + } + } + + abstract protected override def runTest(testName: String, args: Args): Status = { + @volatile var testCalled = false + @volatile var afterTestCalled = false + + try { + startedContainers.foreach(beforeTest) + + testCalled = true + val status = super.runTest(testName, args) + + afterTestCalled = true + if (!status.succeeds()) { + val err = new RuntimeException("Test failed") + startedContainers.foreach(afterTest(_, Some(err))) + } else { + startedContainers.foreach(afterTest(_, None)) + } + + status + } + catch { + case e: Throwable => + if (testCalled && !afterTestCalled) { + afterTestCalled = true + startedContainers.foreach(afterTest(_, Some(e))) + } + + throw e + } + } +} diff --git a/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainersForEach.scala b/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainersForEach.scala new file mode 100644 index 00000000..b286c946 --- /dev/null +++ b/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainersForEach.scala @@ -0,0 +1,82 @@ +package com.dimafeng.testcontainers.scalatest + +import org.scalatest.{Args, Status, Suite} + +/** + * Starts containers before each test and stop them after each test + * + * Example: + * {{{ + * class ExampleSpec extends FlatSpec with TestContainersForEach { + * + * // First of all, you need to declare, which containers you want to use + * override type Containers = MySQLContainer and PostgreSQLContainer + * + * // After that, you need to describe, how you want to start them, + * // In this method you can use any intermediate logic. + * // You can pass parameters between containers, for example. + * override def startContainers(): Containers = { + * val container1 = MySQLContainer.Def().start() + * val container2 = PostgreSQLContainer.Def().start() + * container1 and container2 + * } + * + * // `withContainers` function supports multiple containers: + * it should "test" in withContainers { case mysqlContainer and pgContainer => + * // Inside your test body you can do with your containers whatever you want to + * assert(mysqlContainer.jdbcUrl.nonEmpty && pgContainer.jdbcUrl.nonEmpty) + * } + * + * } + * }}} + */ +trait TestContainersForEach extends TestContainersSuite { self: Suite => + + abstract protected override def runTest(testName: String, args: Args): Status = { + val containers = startContainers() + startedContainers = Some(containers) + + @volatile var testCalled = false + @volatile var afterTestCalled = false + + try { + afterStart() + beforeTest(containers) + + testCalled = true + val status = super.runTest(testName, args) + + afterTestCalled = true + if (!status.succeeds()) { + val err = new RuntimeException("Test failed") + startedContainers.foreach(afterTest(_, Some(err))) + } else { + startedContainers.foreach(afterTest(_, None)) + } + + status + } + catch { + case e: Throwable => + if (testCalled && !afterTestCalled) { + afterTestCalled = true + afterTest(containers, Some(e)) + } + + throw e + } + finally { + try { + beforeStop() + } + finally { + try { + startedContainers.foreach(_.stop()) + } + finally { + startedContainers = None + } + } + } + } +} diff --git a/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainersSuite.scala b/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainersSuite.scala new file mode 100644 index 00000000..e9bde444 --- /dev/null +++ b/test-framework/scalatest/src/main/scala/com/dimafeng/testcontainers/scalatest/TestContainersSuite.scala @@ -0,0 +1,101 @@ +package com.dimafeng.testcontainers.scalatest + +import com.dimafeng.testcontainers.TestContainers +import com.dimafeng.testcontainers.lifecycle.{Andable, TestLifecycleAware} +import org.scalatest.{Suite, SuiteMixin} + +/** + * Base trait for all scalatest suites. Not for direct usage. + */ +private[scalatest] trait TestContainersSuite extends SuiteMixin { self: Suite => + + import TestContainers._ + + /** + * To use testcontainers scalatest suites you need to declare, + * which containers you want to use inside your tests. + * + * For example: + * {{{ + * override type Containers = MySQLContainer + * }}} + * + * If you want to use multiple containers inside your tests, use `and` syntax: + * {{{ + * override type Containers = MySQLContainer and PostgreSQLContainer + * }}} + */ + type Containers <: Andable + + /** + * Contains containers startup logic. + * In this method you can use any intermediate logic. + * You can pass parameters between containers, for example: + * {{{ + * override def startContainers(): Containers = { + * val container1 = Container1.Def().start() + * val container2 = Container2.Def(container1.someParam).start() + * container1 and container2 + * } + * }}} + * + * @return Started containers + */ + def startContainers(): Containers + + /** + * To use containers inside your test bodies you need to use `withContainers` function: + * {{{ + * it should "test" in withContainers { mysqlContainer => + * // Inside your test body you can do with your container whatever you want to + * assert(mysqlContainer.jdbcUrl.nonEmpty) + * } + * }}} + * + * `withContainers` also supports multiple containers: + * {{{ + * it should "test" in withContainers { case mysqlContainer and pgContainer => + * // test body + * } + * }}} + * + * @param runTest Test body + */ + def withContainers(runTest: Containers => Unit): Unit = { + val c = startedContainers.getOrElse(throw IllegalWithContainersCall()) + runTest(c) + } + + /** + * Override, if you want to do something after containers start. + */ + def afterStart(): Unit = {} + + /** + * Override, if you want to do something before containers stop. + */ + def beforeStop(): Unit = {} + + @volatile private[testcontainers] var startedContainers: Option[Containers] = None + + private val suiteDescription = createDescription(self) + + 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 + } + } +} + +case class IllegalWithContainersCall() extends IllegalStateException( + "'withContainers' method can't be used before all containers are started. " + + "'withContainers' method should be used only in test cases to prevent this." +) diff --git a/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/ContainerSpec.scala b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/ContainerSpec.scala index cc1ad826..7735e24b 100644 --- a/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/ContainerSpec.scala +++ b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/ContainerSpec.scala @@ -8,13 +8,12 @@ import org.mockito.ArgumentMatchers._ 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] { it should "call all appropriate methods of the container" in { - val container = mock[SampleOTCContainer] + val container = mock[SampleJavaContainer] new TestSpec({ assert(1 == 1) @@ -27,7 +26,7 @@ class ContainerSpec extends BaseSpec[ForEachTestContainer] { } it should "call all appropriate methods of the container if assertion fails" in { - val container = mock[SampleOTCContainer] + val container = mock[SampleJavaContainer] var err: Throwable = null @@ -44,7 +43,7 @@ class ContainerSpec extends BaseSpec[ForEachTestContainer] { } it should "start and stop container only once" in { - val container = mock[SampleOTCContainer] + val container = mock[SampleJavaContainer] new MultipleTestsSpec({ assert(1 == 1) @@ -57,7 +56,7 @@ class ContainerSpec extends BaseSpec[ForEachTestContainer] { } it should "call afterStart() and beforeStop()" in { - val container = mock[SampleOTCContainer] + val container = mock[SampleJavaContainer] // ForEach val specForEach = Mockito.spy(new TestSpec({}, new SampleContainer(container))) @@ -76,7 +75,7 @@ class ContainerSpec extends BaseSpec[ForEachTestContainer] { } it should "call beforeStop() and stop container if error thrown in afterStart()" in { - val container = mock[SampleOTCContainer] + val container = mock[SampleJavaContainer] // ForEach val specForEach = Mockito.spy(new TestSpecWithFailedAfterStart({}, new SampleContainer(container))) @@ -104,7 +103,7 @@ class ContainerSpec extends BaseSpec[ForEachTestContainer] { } it should "not start container if all tests are ignored" in { - val container = mock[SampleOTCContainer] + val container = mock[SampleJavaContainer] val specForAll = Mockito.spy(new TestSpecWithAllIgnored({}, new SampleContainer(container))) specForAll.run(None, Args(mock[Reporter])) @@ -112,7 +111,7 @@ class ContainerSpec extends BaseSpec[ForEachTestContainer] { } it should "work with `configure` method" in { - val innerContainer = new SampleOTCContainer + val innerContainer = new SampleJavaContainer val container = new SampleContainer(innerContainer) .configure{c => c.withWorkingDirectory("123"); ()} @@ -173,36 +172,4 @@ object ContainerSpec { testBody } } - - class SampleOTCContainer extends OTCGenericContainer with JavaTestLifecycleAware { - - override def beforeTest(description: TestDescription): Unit = { - println("beforeTest") - } - - override def afterTest(description: TestDescription, throwable: Optional[Throwable]): Unit = { - println("afterTest") - } - - override def start(): Unit = { - println("start") - } - - override def stop(): Unit = { - println("stop") - } - } - - 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/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/MultipleContainersSpec.scala b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/MultipleContainersSpec.scala index 8d0f6dff..3620dfff 100644 --- a/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/MultipleContainersSpec.scala +++ b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/MultipleContainersSpec.scala @@ -2,9 +2,7 @@ package com.dimafeng.testcontainers import java.util.Optional -import com.dimafeng.testcontainers.ContainerSpec.{SampleContainer, SampleOTCContainer} 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.verify @@ -13,8 +11,8 @@ import org.scalatestplus.mockito.MockitoSugar class MultipleContainersSpec extends BaseSpec[ForEachTestContainer] { it should "call all expected methods of the multiple containers" in { - val container1 = mock[SampleOTCContainer] - val container2 = mock[SampleOTCContainer] + val container1 = mock[SampleJavaContainer] + val container2 = mock[SampleJavaContainer] val containers = MultipleContainers(new SampleContainer(container1), new SampleContainer(container2)) @@ -53,8 +51,8 @@ class MultipleContainersSpec extends BaseSpec[ForEachTestContainer] { object MultipleContainersSpec { - class InitializableContainer(valueToBeSetAfterStart: String) extends SingleContainer[SampleOTCContainer] with MockitoSugar { - override implicit val container: SampleOTCContainer = mock[SampleOTCContainer] + class InitializableContainer(valueToBeSetAfterStart: String) extends SingleContainer[SampleJavaContainer] with MockitoSugar { + override implicit val container: SampleJavaContainer = mock[SampleJavaContainer] var value: String = _ override def start(): Unit = { @@ -62,8 +60,8 @@ object MultipleContainersSpec { } } - class ExampleContainerWithVariable(val variable: String) extends SingleContainer[SampleOTCContainer] with MockitoSugar { - override implicit val container: SampleOTCContainer = mock[SampleOTCContainer] + class ExampleContainerWithVariable(val variable: String) extends SingleContainer[SampleJavaContainer] with MockitoSugar { + override implicit val container: SampleJavaContainer = mock[SampleJavaContainer] } protected class TestSpec(testBody: => Unit, _container: Container) extends FlatSpec with ForEachTestContainer { diff --git a/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/SampleContainer.scala b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/SampleContainer.scala new file mode 100644 index 00000000..919060a6 --- /dev/null +++ b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/SampleContainer.scala @@ -0,0 +1,47 @@ +package com.dimafeng.testcontainers + +import java.util.Optional + +import com.dimafeng.testcontainers.lifecycle.TestLifecycleAware +import org.testcontainers.containers.{GenericContainer => JavaGenericContainer} +import org.testcontainers.lifecycle.{TestDescription, TestLifecycleAware => JavaTestLifecycleAware} + +class SampleJavaContainer extends JavaGenericContainer with JavaTestLifecycleAware { + + override def beforeTest(description: TestDescription): Unit = { + println("beforeTest") + } + + override def afterTest(description: TestDescription, throwable: Optional[Throwable]): Unit = { + println("afterTest") + } + + override def start(): Unit = { + println("start") + } + + override def stop(): Unit = { + println("stop") + } +} + +case class SampleContainer(sampleJavaContainer: SampleJavaContainer) + extends SingleContainer[SampleJavaContainer] with TestLifecycleAware { + override implicit val container: 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 { + case class Def(sampleJavaContainer: SampleJavaContainer) extends ContainerDef { + override type Container = SampleContainer + override protected def createContainer(): SampleContainer = { + SampleContainer(sampleJavaContainer) + } + } +} diff --git a/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/integration/GenericContainerDefSpec.scala b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/integration/GenericContainerDefSpec.scala new file mode 100644 index 00000000..7342923b --- /dev/null +++ b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/integration/GenericContainerDefSpec.scala @@ -0,0 +1,73 @@ +package com.dimafeng.testcontainers.integration + +import java.net.URL + +import com.dimafeng.testcontainers.{GenericContainer, SingleContainer} +import com.dimafeng.testcontainers.lifecycle.and +import com.dimafeng.testcontainers.scalatest.TestContainersForAll +import org.scalatest.FlatSpec +import org.testcontainers.containers.wait.strategy.Wait + +import scala.io.Source + +class GenericContainerDefSpec extends FlatSpec with TestContainersForAll { + + import GenericContainerDefSpec._ + + override type Containers = CompatibleGenericContainer and NotCompatibleGenericContainer + + override def startContainers(): Containers = { + val compatible = CompatibleGenericContainer.Def().start() + val notCompatible = NotCompatibleGenericContainer.Def().start() + compatible and notCompatible + } + + "GenericContainer.Def" should "be able to work through compatible and not compatible constructors" in withContainers { + case compatible and notCompatible => + val expectedText = "If you see this page, the nginx web server is successfully installed" + assert( + compatible.rootPage.contains(expectedText) && + notCompatible.rootPage.contains(expectedText) + ) + } +} +object GenericContainerDefSpec { + + private val port = 80 + + private def createUrl(container: SingleContainer[_]) = { + new URL(s"http://${container.containerIpAddress}:${container.mappedPort(port)}/") + } + + private def urlToString(url: URL) = { + Source.fromInputStream(url.openConnection().getInputStream).mkString + } + + class CompatibleGenericContainer extends GenericContainer( + dockerImage = "nginx:latest", + exposedPorts = Seq(port), + waitStrategy = Some(Wait.forHttp("/")) + ) { + def rootUrl: URL = createUrl(this) + def rootPage: String = urlToString(rootUrl) + } + object CompatibleGenericContainer { + case class Def() extends GenericContainer.Def[CompatibleGenericContainer]( + new CompatibleGenericContainer() + ) + } + + class NotCompatibleGenericContainer(underlying: GenericContainer) extends GenericContainer(underlying) { + def rootUrl: URL = createUrl(this) + def rootPage: String = urlToString(rootUrl) + } + object NotCompatibleGenericContainer { + case class Def() extends GenericContainer.Def[NotCompatibleGenericContainer]( + new NotCompatibleGenericContainer(GenericContainer( + dockerImage = "nginx:latest", + exposedPorts = Seq(port), + waitStrategy = Wait.forHttp("/") + )) + ) + } +} diff --git a/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/scalatest/TestContainerForAllSpec.scala b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/scalatest/TestContainerForAllSpec.scala new file mode 100644 index 00000000..1e737cdc --- /dev/null +++ b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/scalatest/TestContainerForAllSpec.scala @@ -0,0 +1,138 @@ +package com.dimafeng.testcontainers.scalatest + +import java.util.Optional + +import com.dimafeng.testcontainers.{BaseSpec, ContainerDef, SampleContainer, SampleJavaContainer} +import org.mockito.ArgumentMatchers._ +import org.mockito.Mockito.{times, verify} +import org.mockito.{ArgumentCaptor, ArgumentMatchers, Mockito} +import org.scalatest.{Args, FlatSpec, Reporter} + +class TestContainerForAllSpec extends BaseSpec[TestContainerForAll] { + + import TestContainerForAllSpec._ + + it should "call all appropriate methods of the container" in { + val container = mock[SampleJavaContainer] + + new TestSpec({ + assert(1 == 1) + }, SampleContainer.Def(container)).run(None, Args(mock[Reporter])) + + 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[SampleJavaContainer] + + new TestSpec({ + assert(1 == 2) + }, SampleContainer.Def(container)).run(None, Args(mock[Reporter])) + + 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 { + val container = mock[SampleJavaContainer] + + new MultipleTestsSpec({ + assert(1 == 1) + }, SampleContainer.Def(container)).run(None, Args(mock[Reporter])) + + 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 { + val container = mock[SampleJavaContainer] + + val spec = Mockito.spy(new MultipleTestsSpec({}, SampleContainer.Def(container))) + spec.run(None, Args(mock[Reporter])) + + verify(spec).afterStart() + verify(spec).beforeStop() + } + + it should "call beforeStop() and stop container if error thrown in afterStart()" in { + val container = mock[SampleJavaContainer] + + val spec = Mockito.spy(new MultipleTestsSpecWithFailedAfterStart({}, SampleContainer.Def(container))) + intercept[RuntimeException] { + spec.run(None, Args(mock[Reporter])) + } + verify(container, times(0)).beforeTest(any()) + verify(container).start() + verify(spec).afterStart() + verify(container, times(0)).afterTest(any(), any()) + verify(spec).beforeStop() + verify(container).stop() + } + + it should "not start container if all tests are ignored" in { + val container = mock[SampleJavaContainer] + val spec = Mockito.spy(new TestSpecWithAllIgnored({}, SampleContainer.Def(container))) + spec.run(None, Args(mock[Reporter])) + + verify(container, Mockito.never()).start() + } +} + +object TestContainerForAllSpec { + + protected class TestSpec(testBody: => Unit, contDef: ContainerDef) + extends FlatSpec with TestContainerForAll { + + override val containerDef: ContainerDef = contDef + + it should "test" in { + testBody + } + } + + protected class MultipleTestsSpec(testBody: => Unit, contDef: ContainerDef) extends FlatSpec with TestContainerForAll { + + override val containerDef: ContainerDef = contDef + + it should "test1" in { + testBody + } + + it should "test2" in { + testBody + } + } + + protected class MultipleTestsSpecWithFailedAfterStart(testBody: => Unit, contDef: ContainerDef) extends FlatSpec with TestContainerForAll { + + override val containerDef: ContainerDef = contDef + + override def afterStart(): Unit = throw new RuntimeException("something wrong in afterStart()") + + it should "test1" in { + testBody + } + + it should "test2" in { + testBody + } + } + + protected class TestSpecWithAllIgnored(testBody: => Unit, contDef: ContainerDef) extends FlatSpec with TestContainerForAll { + + override val containerDef: ContainerDef = contDef + + it should "test" ignore { + testBody + } + } +} diff --git a/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/scalatest/TestContainerForEachSpec.scala b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/scalatest/TestContainerForEachSpec.scala new file mode 100644 index 00000000..c3c1b81b --- /dev/null +++ b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/scalatest/TestContainerForEachSpec.scala @@ -0,0 +1,138 @@ +package com.dimafeng.testcontainers.scalatest + +import java.util.Optional + +import com.dimafeng.testcontainers.{BaseSpec, ContainerDef, SampleContainer, SampleJavaContainer} +import org.mockito.ArgumentMatchers._ +import org.mockito.Mockito.{times, verify} +import org.mockito.{ArgumentCaptor, ArgumentMatchers, Mockito} +import org.scalatest.{Args, FlatSpec, Reporter} + +class TestContainerForEachSpec extends BaseSpec[TestContainerForEach] { + + import TestContainerForEachSpec._ + + it should "call all appropriate methods of the container" in { + val container = mock[SampleJavaContainer] + + new TestSpec({ + assert(1 == 1) + }, SampleContainer.Def(container)).run(None, Args(mock[Reporter])) + + 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[SampleJavaContainer] + + new TestSpec({ + assert(1 == 2) + }, SampleContainer.Def(container)).run(None, Args(mock[Reporter])) + + 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 before and after each test case" in { + val container = mock[SampleJavaContainer] + + new MultipleTestsSpec({ + assert(1 == 1) + }, SampleContainer.Def(container)).run(None, Args(mock[Reporter])) + + verify(container, times(2)).beforeTest(any()) + verify(container, times(2)).start() + verify(container, times(2)).afterTest(any(), any()) + verify(container, times(2)).stop() + } + + it should "call afterStart() and beforeStop()" in { + val container = mock[SampleJavaContainer] + + val spec = Mockito.spy(new MultipleTestsSpec({}, SampleContainer.Def(container))) + spec.run(None, Args(mock[Reporter])) + + verify(spec, times(2)).afterStart() + verify(spec, times(2)).beforeStop() + } + + it should "call beforeStop() and stop container if error thrown in afterStart()" in { + val container = mock[SampleJavaContainer] + + val spec = Mockito.spy(new MultipleTestsSpecWithFailedAfterStart({}, SampleContainer.Def(container))) + intercept[RuntimeException] { + spec.run(None, Args(mock[Reporter])) + } + verify(container, times(0)).beforeTest(any()) + verify(container).start() + verify(spec).afterStart() + verify(container, times(0)).afterTest(any(), any()) + verify(spec).beforeStop() + verify(container).stop() + } + + it should "not start container if all tests are ignored" in { + val container = mock[SampleJavaContainer] + val spec = Mockito.spy(new TestSpecWithAllIgnored({}, SampleContainer.Def(container))) + spec.run(None, Args(mock[Reporter])) + + verify(container, Mockito.never()).start() + } +} + +object TestContainerForEachSpec { + + protected class TestSpec(testBody: => Unit, contDef: ContainerDef) + extends FlatSpec with TestContainerForEach { + + override val containerDef: ContainerDef = contDef + + it should "test" in { + testBody + } + } + + protected class MultipleTestsSpec(testBody: => Unit, contDef: ContainerDef) extends FlatSpec with TestContainerForEach { + + override val containerDef: ContainerDef = contDef + + it should "test1" in { + testBody + } + + it should "test2" in { + testBody + } + } + + protected class MultipleTestsSpecWithFailedAfterStart(testBody: => Unit, contDef: ContainerDef) extends FlatSpec with TestContainerForEach { + + override val containerDef: ContainerDef = contDef + + override def afterStart(): Unit = throw new RuntimeException("something wrong in afterStart()") + + it should "test1" in { + testBody + } + + it should "test2" in { + testBody + } + } + + protected class TestSpecWithAllIgnored(testBody: => Unit, contDef: ContainerDef) extends FlatSpec with TestContainerForEach { + + override val containerDef: ContainerDef = contDef + + it should "test" ignore { + testBody + } + } +} diff --git a/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/scalatest/TestContainersForAllSpec.scala b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/scalatest/TestContainersForAllSpec.scala new file mode 100644 index 00000000..70d67f5b --- /dev/null +++ b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/scalatest/TestContainersForAllSpec.scala @@ -0,0 +1,218 @@ +package com.dimafeng.testcontainers.scalatest + +import java.util.Optional + +import com.dimafeng.testcontainers.lifecycle.and +import com.dimafeng.testcontainers.{BaseSpec, SampleContainer, SampleJavaContainer} +import org.mockito.ArgumentMatchers._ +import org.mockito.Mockito.{times, verify} +import org.mockito.{ArgumentCaptor, ArgumentMatchers, Mockito} +import org.scalatest.{Args, FlatSpec, Reporter} + +class TestContainersForAllSpec extends BaseSpec[TestContainersForAll] { + + import TestContainersForAllSpec._ + + it should "call all appropriate methods of the containers" in { + val container1 = mock[SampleJavaContainer] + val container2 = mock[SampleJavaContainer] + + val res = new TestSpec({ + assert(1 == 1) + }, container1, container2).run(None, Args(mock[Reporter])) + + assert(res.succeeds()) + + 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() + } + + it should "call all appropriate methods of the containers if assertion fails" in { + val container1 = mock[SampleJavaContainer] + val container2 = mock[SampleJavaContainer] + + val res = new TestSpec({ + assert(1 == 2) + }, container1, container2).run(None, Args(mock[Reporter])) + + assert(!res.succeeds()) + + val captor1 = ArgumentCaptor.forClass[Optional[Throwable], Optional[Throwable]](classOf[Optional[Throwable]]) + verify(container1).beforeTest(any()) + verify(container1).start() + verify(container1).afterTest(any(), captor1.capture()) + assert(captor1.getValue.isPresent) + verify(container1).stop() + + val captor2 = ArgumentCaptor.forClass[Optional[Throwable], Optional[Throwable]](classOf[Optional[Throwable]]) + verify(container2).beforeTest(any()) + verify(container2).start() + verify(container2).afterTest(any(), captor2.capture()) + assert(captor2.getValue.isPresent) + verify(container2).stop() + } + + it should "start and stop containers only once" in { + val container1 = mock[SampleJavaContainer] + val container2 = mock[SampleJavaContainer] + + val res = new MultipleTestsSpec({ + assert(1 == 1) + }, container1, container2).run(None, Args(mock[Reporter])) + + assert(res.succeeds()) + + verify(container1, times(2)).beforeTest(any()) + verify(container1).start() + verify(container1, times(2)).afterTest(any(), any()) + verify(container1).stop() + + verify(container2, times(2)).beforeTest(any()) + verify(container2).start() + verify(container2, times(2)).afterTest(any(), any()) + verify(container2).stop() + } + + it should "call afterStart() and beforeStop()" in { + val container1 = mock[SampleJavaContainer] + val container2 = mock[SampleJavaContainer] + + // Mockito somehow messed up internal state, so we can't use `spy` here. + @volatile var afterStartCalled = false + @volatile var beforeStopCalled = false + + val spec = new MultipleTestsSpec({ + assert(1 == 1) + }, container1, container2) { + override def afterStart(): Unit = { + super.afterStart() + afterStartCalled = true + } + + override def beforeStop(): Unit = { + super.beforeStop() + beforeStopCalled = true + } + } + + val res = spec.run(None, Args(mock[Reporter])) + + assert(res.succeeds() && afterStartCalled && beforeStopCalled) + } + + it should "call beforeStop() and stop container if error thrown in afterStart()" in { + val container1 = mock[SampleJavaContainer] + val container2 = mock[SampleJavaContainer] + + @volatile var afterStartCalled = false + @volatile var beforeStopCalled = false + + val spec = new MultipleTestsSpec({ + assert(1 == 1) + }, container1, container2) { + override def afterStart(): Unit = { + afterStartCalled = true + throw new RuntimeException("Test") + } + + override def beforeStop(): Unit = { + super.beforeStop() + beforeStopCalled = true + } + } + + val res = intercept[RuntimeException] { + spec.run(None, Args(mock[Reporter])) + } + + verify(container1, times(0)).beforeTest(any()) + verify(container1).start() + verify(container1, times(0)).afterTest(any(), any()) + verify(container1).stop() + + verify(container2, times(0)).beforeTest(any()) + verify(container2).start() + verify(container2, times(0)).afterTest(any(), any()) + verify(container2).stop() + + assert(res.getMessage === "Test" && afterStartCalled && beforeStopCalled) + } + + it should "not start container if all tests are ignored" in { + val container1 = mock[SampleJavaContainer] + val container2 = mock[SampleJavaContainer] + + @volatile var called = false + + new TestSpecWithAllIgnored({ + called = true + }, container1, container2) {}.run(None, Args(mock[Reporter])) + + verify(container1, Mockito.never()).start() + verify(container2, Mockito.never()).start() + assert(called === false) + } +} +object TestContainersForAllSpec { + + protected abstract class AbstractTestSpec( + testBody: => Unit, + container1: SampleJavaContainer, + container2: SampleJavaContainer + ) extends FlatSpec with TestContainersForAll { + override type Containers = SampleContainer and SampleContainer + + override def startContainers(): Containers = { + val c1 = SampleContainer.Def(container1).start() + val c2 = SampleContainer.Def(container2).start() + c1 and c2 + } + } + + protected class TestSpec(testBody: => Unit, container1: SampleJavaContainer, container2: SampleJavaContainer) + extends AbstractTestSpec(testBody, container1, container2) { + + it should "test" in withContainers { case c1 and c2 => + assert( + c1.underlyingUnsafeContainer === container1 && + c2.underlyingUnsafeContainer === container2 + ) + testBody + } + } + + protected class MultipleTestsSpec(testBody: => Unit, container1: SampleJavaContainer, container2: SampleJavaContainer) + extends AbstractTestSpec(testBody, container1, container2) { + + it should "test1" in withContainers { case c1 and c2 => + assert( + c1.underlyingUnsafeContainer === container1 && + c2.underlyingUnsafeContainer === container2 + ) + testBody + } + + it should "test2" in withContainers { case c1 and c2 => + assert( + c1.underlyingUnsafeContainer === container1 && + c2.underlyingUnsafeContainer === container2 + ) + testBody + } + } + + protected class TestSpecWithAllIgnored(testBody: => Unit, container1: SampleJavaContainer, container2: SampleJavaContainer) + extends AbstractTestSpec(testBody, container1, container2) { + + it should "test" ignore { + testBody + } + } +} diff --git a/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/scalatest/TestContainersForEachSpec.scala b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/scalatest/TestContainersForEachSpec.scala new file mode 100644 index 00000000..0d342c42 --- /dev/null +++ b/test-framework/scalatest/src/test/scala/com/dimafeng/testcontainers/scalatest/TestContainersForEachSpec.scala @@ -0,0 +1,218 @@ +package com.dimafeng.testcontainers.scalatest + +import java.util.Optional + +import com.dimafeng.testcontainers.lifecycle.and +import com.dimafeng.testcontainers.{BaseSpec, SampleContainer, SampleJavaContainer} +import org.mockito.ArgumentMatchers._ +import org.mockito.Mockito.{times, verify} +import org.mockito.{ArgumentCaptor, ArgumentMatchers, Mockito} +import org.scalatest.{Args, FlatSpec, Reporter} + +class TestContainersForEachSpec extends BaseSpec[TestContainersForEach] { + + import TestContainersForEachSpec._ + + it should "call all appropriate methods of the containers" in { + val container1 = mock[SampleJavaContainer] + val container2 = mock[SampleJavaContainer] + + val res = new TestSpec({ + assert(1 == 1) + }, container1, container2).run(None, Args(mock[Reporter])) + + assert(res.succeeds()) + + 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() + } + + it should "call all appropriate methods of the containers if assertion fails" in { + val container1 = mock[SampleJavaContainer] + val container2 = mock[SampleJavaContainer] + + val res = new TestSpec({ + assert(1 == 2) + }, container1, container2).run(None, Args(mock[Reporter])) + + assert(!res.succeeds()) + + val captor1 = ArgumentCaptor.forClass[Optional[Throwable], Optional[Throwable]](classOf[Optional[Throwable]]) + verify(container1).beforeTest(any()) + verify(container1).start() + verify(container1).afterTest(any(), captor1.capture()) + assert(captor1.getValue.isPresent) + verify(container1).stop() + + val captor2 = ArgumentCaptor.forClass[Optional[Throwable], Optional[Throwable]](classOf[Optional[Throwable]]) + verify(container2).beforeTest(any()) + verify(container2).start() + verify(container2).afterTest(any(), captor2.capture()) + assert(captor2.getValue.isPresent) + verify(container2).stop() + } + + it should "start and stop containers before and after each test case" in { + val container1 = mock[SampleJavaContainer] + val container2 = mock[SampleJavaContainer] + + val res = new MultipleTestsSpec({ + assert(1 == 1) + }, container1, container2).run(None, Args(mock[Reporter])) + + assert(res.succeeds()) + + verify(container1, times(2)).beforeTest(any()) + verify(container1, times(2)).start() + verify(container1, times(2)).afterTest(any(), any()) + verify(container1, times(2)).stop() + + verify(container2, times(2)).beforeTest(any()) + verify(container2, times(2)).start() + verify(container2, times(2)).afterTest(any(), any()) + verify(container2, times(2)).stop() + } + + it should "call afterStart() and beforeStop()" in { + val container1 = mock[SampleJavaContainer] + val container2 = mock[SampleJavaContainer] + + // Mockito somehow messed up internal state, so we can't use `spy` here. + @volatile var afterStartCalled = false + @volatile var beforeStopCalled = false + + val spec = new MultipleTestsSpec({ + assert(1 == 1) + }, container1, container2) { + override def afterStart(): Unit = { + super.afterStart() + afterStartCalled = true + } + + override def beforeStop(): Unit = { + super.beforeStop() + beforeStopCalled = true + } + } + + val res = spec.run(None, Args(mock[Reporter])) + + assert(res.succeeds() && afterStartCalled && beforeStopCalled) + } + + it should "call beforeStop() and stop container if error thrown in afterStart()" in { + val container1 = mock[SampleJavaContainer] + val container2 = mock[SampleJavaContainer] + + @volatile var afterStartCalled = false + @volatile var beforeStopCalled = false + + val spec = new MultipleTestsSpec({ + assert(1 == 1) + }, container1, container2) { + override def afterStart(): Unit = { + afterStartCalled = true + throw new RuntimeException("Test") + } + + override def beforeStop(): Unit = { + super.beforeStop() + beforeStopCalled = true + } + } + + val res = intercept[RuntimeException] { + spec.run(None, Args(mock[Reporter])) + } + + verify(container1, times(0)).beforeTest(any()) + verify(container1).start() + verify(container1, times(0)).afterTest(any(), any()) + verify(container1).stop() + + verify(container2, times(0)).beforeTest(any()) + verify(container2).start() + verify(container2, times(0)).afterTest(any(), any()) + verify(container2).stop() + + assert(res.getMessage === "Test" && afterStartCalled && beforeStopCalled) + } + + it should "not start container if all tests are ignored" in { + val container1 = mock[SampleJavaContainer] + val container2 = mock[SampleJavaContainer] + + @volatile var called = false + + new TestSpecWithAllIgnored({ + called = true + }, container1, container2) {}.run(None, Args(mock[Reporter])) + + verify(container1, Mockito.never()).start() + verify(container2, Mockito.never()).start() + assert(called === false) + } +} +object TestContainersForEachSpec { + + protected abstract class AbstractTestSpec( + testBody: => Unit, + container1: SampleJavaContainer, + container2: SampleJavaContainer + ) extends FlatSpec with TestContainersForEach { + override type Containers = SampleContainer and SampleContainer + + override def startContainers(): Containers = { + val c1 = SampleContainer.Def(container1).start() + val c2 = SampleContainer.Def(container2).start() + c1 and c2 + } + } + + protected class TestSpec(testBody: => Unit, container1: SampleJavaContainer, container2: SampleJavaContainer) + extends AbstractTestSpec(testBody, container1, container2) { + + it should "test" in withContainers { case c1 and c2 => + assert( + c1.underlyingUnsafeContainer === container1 && + c2.underlyingUnsafeContainer === container2 + ) + testBody + } + } + + protected class MultipleTestsSpec(testBody: => Unit, container1: SampleJavaContainer, container2: SampleJavaContainer) + extends AbstractTestSpec(testBody, container1, container2) { + + it should "test1" in withContainers { case c1 and c2 => + assert( + c1.underlyingUnsafeContainer === container1 && + c2.underlyingUnsafeContainer === container2 + ) + testBody + } + + it should "test2" in withContainers { case c1 and c2 => + assert( + c1.underlyingUnsafeContainer === container1 && + c2.underlyingUnsafeContainer === container2 + ) + testBody + } + } + + protected class TestSpecWithAllIgnored(testBody: => Unit, container1: SampleJavaContainer, container2: SampleJavaContainer) + extends AbstractTestSpec(testBody, container1, container2) { + + it should "test" ignore { + testBody + } + } +} diff --git a/version.sbt b/version.sbt index 3d4d7056..bd8ee588 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "0.33.1-SNAPSHOT" +version in ThisBuild := "0.34.0-SNAPSHOT"