diff --git a/README.md b/README.md index 10904a2b..f4369e21 100644 --- a/README.md +++ b/README.md @@ -491,6 +491,12 @@ Similarly to Scalatest, you can use one of the four traits: 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. +Additionally, you have available MUnit fixtures integrations under `TestContainersFixtures`: + +1. `ForAllContainerFixture` — will start a single container before all tests and stop after all tests. +2. `ForEachContainerFixture` — will start a single container before each test and stop after each test. +3. `ContainerFunFixture` — will start a single container before each test and stop after each test. + #### Single container in tests If you want to use a single container for all tests in your suite: @@ -613,6 +619,40 @@ class MySpec extends AnyFlatSpec with TestContainersForAll { } ``` +#### Using fixtures + +Instead of the `*ForAll`/`*ForEach` traits you can use the fixtures under `TestContainersFixture`: + +```scala +class MysqlSpec extends FunSuite with TestContainersFixtures { + + // Use `ForAllContainerFixture` to start/stop container before/after all tests + val mysql = ForEachContainerFixture(MySQLContainer()) + + // You need to override `munitFixtures` and pass in your container fixture + override def munitFixtures = List(mysql) + + test("test case name") { + // Inside your test body you can do with your container whatever you want to + assert(mysql().jdbcUrl.nonEmpty) + } +} +``` + +There is also available a `FunFixture` version for containers: + +```scala +class MysqlSpec extends FunSuite with TestContainersFixtures { + + val mysql = ContainerFunFixture(MySQLContainer()) + + mysql.test("test case name") { container => + // Inside your test body you can do with your container whatever you want to + assert(container.jdbcUrl.nonEmpty) + } +} +``` + #### Notes on MUnit usage - If you use `*ForAll` trait and override beforeAll() without calling super.beforeAll() your containers won't start. - If you use `*ForAll` trait and override afterAll() without calling super.afterAll() your containers won't stop. diff --git a/test-framework/munit/src/main/scala/com/dimafeng/testcontainers/munit/fixtures/TestContainersFixtures.scala b/test-framework/munit/src/main/scala/com/dimafeng/testcontainers/munit/fixtures/TestContainersFixtures.scala new file mode 100644 index 00000000..06850238 --- /dev/null +++ b/test-framework/munit/src/main/scala/com/dimafeng/testcontainers/munit/fixtures/TestContainersFixtures.scala @@ -0,0 +1,205 @@ +package com.dimafeng.testcontainers.munit.fixtures + +import com.dimafeng.testcontainers.lifecycle.Stoppable +import org.testcontainers.lifecycle.Startable +import munit.Suite +import munit.FunFixtures +import munit.FunSuite +import com.dimafeng.testcontainers.munit.TestContainersSuite +import com.dimafeng.testcontainers.lifecycle.TestLifecycleAware + +trait TestContainersFixtures { self: FunSuite => + + /** + * Creates a fixture that starts a single container before each test and stops + * it after each test. + * + * @example + * {{{ + * class MysqlSpec extends FunSuite with TestContainersFixtures { + * + * val mysql = ForEachContainerFixture(MySQLContainer()) + * + * // You need to override `munitFixtures` and pass in your container fixture + * override def munitFixtures = List(mysql) + * + * test("test case name") { + * // Inside your test body you can do with your container whatever you want to + * assert(mysql().jdbcUrl.nonEmpty) + * } + * } + * }}} + */ + class ForEachContainerFixture[T <: Startable with Stoppable](val container: T) + extends Fixture[T]("ForEachTestContainers") { + + def apply(): T = container + + def afterContainerStart(container: T, context: BeforeEach): Unit = () + + def beforeContainerStop(container: T, context: AfterEach): Unit = () + + override def beforeEach(context: BeforeEach): Unit = { + container.start() + afterContainerStart(container, context) + container match { + case container: TestLifecycleAware => container.beforeTest(suiteDescription) + case _ => // do nothing + } + } + + override def afterEach(context: AfterEach): Unit = { + container match { + case container: TestLifecycleAware => container.afterTest(suiteDescription, None) + case _ => // do nothing + } + beforeContainerStop(container, context) + container.stop() + } + + } + + object ForEachContainerFixture { + + /** + * Creates a fixture that starts a single container before each test and stops + * it after each test. + * + * @example + * {{{ + * class MysqlSpec extends FunSuite with TestContainersFixtures { + * + * val mysql = ForEachContainerFixture(MySQLContainer()) + * + * // You need to override `munitFixtures` and pass in your container fixture + * override def munitFixtures = List(mysql) + * + * test("test case name") { + * // Inside your test body you can do with your container whatever you want to + * assert(mysql().jdbcUrl.nonEmpty) + * } + * } + * }}} + */ + def apply[T <: Startable with Stoppable](container: T) = new ForEachContainerFixture[T](container) + + } + + /** + * Creates a fixture that starts a single container before all test and stops + * it after all test. + * + * @example + * {{{ + * class MysqlSpec extends FunSuite with TestContainersFixtures { + * + * val mysql = ForAllContainerFixture(MySQLContainer()) + * + * // You need to override `munitFixtures` and pass in your container fixture + * override def munitFixtures = List(mysql) + * + * test("test case name") { + * // Inside your test body you can do with your container whatever you want to + * assert(mysql().jdbcUrl.nonEmpty) + * } + * } + * }}} + */ + class ForAllContainerFixture[T <: Startable with Stoppable](val container: T) + extends Fixture[T]("ForAllTestContainers") { + + def apply(): T = container + + def afterContainerStart(container: T): Unit = () + + def beforeContainerStop(container: T): Unit = () + + override def beforeAll(): Unit = { + container.start() + afterContainerStart(container) + } + + override def beforeEach(context: BeforeEach): Unit = container match { + case container: TestLifecycleAware => container.beforeTest(suiteDescription) + case _ => // do nothing + } + + override def afterAll(): Unit = { + beforeContainerStop(container) + container.stop() + } + + override def afterEach(context: AfterEach): Unit = container match { + case container: TestLifecycleAware => container.afterTest(suiteDescription, None) + case _ => // do nothing + } + + } + + object ForAllContainerFixture { + + /** + * Creates a fixture that starts a single container before all test and stops + * it after all test. + * + * @example + * {{{ + * class MysqlSpec extends FunSuite with TestContainersFixtures { + * + * val mysql = ForAllContainerFixture(MySQLContainer()) + * + * // You need to override `munitFixtures` and pass in your container fixture + * override def munitFixtures = List(mysql) + * + * test("test case name") { + * // Inside your test body you can do with your container whatever you want to + * assert(mysql().jdbcUrl.nonEmpty) + * } + * } + * }}} + */ + def apply[T <: Startable with Stoppable](container: T) = new ForAllContainerFixture[T](container) + + } + + object ContainerFunFixture { + + /** + * Creates a fun-fixture that starts a single container before each test and stops + * it after each test. + * + * @example + * {{{ + * class MysqlSpec extends FunSuite with TestContainersFixtures { + * + * val mysql = ContainerFunFixture(MySQLContainer()) + * + * mysql.test("test case name") { container => + * // Inside your test body you can do with your container whatever you want to + * assert(container.jdbcUrl.nonEmpty) + * } + * } + * }}} + */ + def apply[T <: Startable with Stoppable](container: T) = FunFixture[T]( + setup = { _ => + container.start(); + container match { + case tla: TestLifecycleAware => tla.beforeTest(suiteDescription) + case _ => // do nothing + } + container + }, teardown = { c => + c match { + case tla: TestLifecycleAware => tla.afterTest(suiteDescription, None) + case _ => // do nothing + } + c.stop() + } + ) + + } + + private val suiteDescription = TestContainersSuite.createDescription(self) + +} diff --git a/test-framework/munit/src/test/scala/com/dimafeng/testcontainers/munit/fixtures/ContainerFunFixtureSpec.scala b/test-framework/munit/src/test/scala/com/dimafeng/testcontainers/munit/fixtures/ContainerFunFixtureSpec.scala new file mode 100644 index 00000000..43498bf0 --- /dev/null +++ b/test-framework/munit/src/test/scala/com/dimafeng/testcontainers/munit/fixtures/ContainerFunFixtureSpec.scala @@ -0,0 +1,77 @@ +package com.dimafeng.testcontainers.munit.fixtures + +import java.util.Optional + +import com.dimafeng.testcontainers.ContainerDef +import com.dimafeng.testcontainers.munit.SampleContainer +import com.dimafeng.testcontainers.munit.SampleContainer.SampleJavaContainer +import com.dimafeng.testcontainers.munit.fixtures.ContainerFunFixtureSpec._ +import munit.{FunSuite, MUnitRunner, Suite} +import org.junit.runner.notification.RunNotifier +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.{times, verify} +import org.mockito.{ArgumentMatchers, Mockito} +import org.scalatestplus.mockito.MockitoSugar +import com.dimafeng.testcontainers.Container + +class ContainerFunFixtureSpec extends FunSuite with MockitoSugar { + test("call all appropriate methods of the containers") { + val container = mock[SampleJavaContainer] + + val spec = new TestSpec(assert(cond = true), SampleContainer(container)) + run(spec) + + verify(container).beforeTest(any()) + verify(container).start() + verify(container).afterTest(any(), ArgumentMatchers.eq(Optional.empty())) + verify(container).stop() + } + + test("call all appropriate methods of the containers if assertion fails") { + val container = mock[SampleJavaContainer] + + val spec = new TestSpec(assert(cond = false), SampleContainer(container)) + + run(spec) + + verify(container).beforeTest(any()) + verify(container).start() + verify(container).afterTest(any(), ArgumentMatchers.eq(Optional.empty())) + verify(container).stop() + } + + test("start and stop containers before and after each test case") { + val container = mock[SampleJavaContainer] + + val spec = new MultipleTestsSpec(assert(cond = true), SampleContainer(container)) + run(spec) + + verify(container, times(2)).beforeTest(any()) + verify(container, times(2)).start() + verify(container, times(2)).afterTest(any(), any()) + verify(container, times(2)).stop() + } + +} + +object ContainerFunFixtureSpec { + protected abstract class ContainerSpec extends FunSuite with TestContainersFixtures + + protected class TestSpec(testBody: => Unit, container: Container) extends ContainerSpec { + val containerFixture = ContainerFunFixture(container) + + containerFixture.test("test") { _ => testBody} + } + + protected class MultipleTestsSpec(testBody: => Unit, container: Container) extends ContainerSpec { + val containerFixture = ContainerFunFixture(container) + + containerFixture.test("test1") { _ => testBody} + containerFixture.test("test2") { _ => testBody} + } + + private def run(res: Suite): Unit = { + val notifier = new RunNotifier() + new MUnitRunner(res.asInstanceOf[Suite].getClass, () => res).run(notifier) + } +} \ No newline at end of file diff --git a/test-framework/munit/src/test/scala/com/dimafeng/testcontainers/munit/fixtures/ForAllTestContainersFixtureSpec.scala b/test-framework/munit/src/test/scala/com/dimafeng/testcontainers/munit/fixtures/ForAllTestContainersFixtureSpec.scala new file mode 100644 index 00000000..2dfff3ed --- /dev/null +++ b/test-framework/munit/src/test/scala/com/dimafeng/testcontainers/munit/fixtures/ForAllTestContainersFixtureSpec.scala @@ -0,0 +1,79 @@ +package com.dimafeng.testcontainers.munit.fixtures + +import java.util.Optional + +import com.dimafeng.testcontainers.ContainerDef +import com.dimafeng.testcontainers.munit.SampleContainer.SampleJavaContainer +import com.dimafeng.testcontainers.munit.fixtures.ForAllTestContainersFixtureSpec._ +import com.dimafeng.testcontainers.munit.SampleContainer +import munit.{FunSuite, MUnitRunner, Suite} +import org.junit.runner.notification.RunNotifier +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.{times, verify} +import org.mockito.{ArgumentMatchers, Mockito} +import org.scalatestplus.mockito.MockitoSugar +import com.dimafeng.testcontainers.SingleContainer +import com.dimafeng.testcontainers.Container + +class ForAllTestContainersFixtureSpec extends FunSuite with MockitoSugar { + test("call all appropriate methods of the container") { + val container = mock[SampleJavaContainer] + + val spec = new TestSpec(assert(cond = true), SampleContainer(container)) + run(spec) + + verify(container).beforeTest(any()) + verify(container).start() + verify(container).afterTest(any(), ArgumentMatchers.eq(Optional.empty())) + verify(container).stop() + } + + test("call all appropriate methods of the container if assertion fails") { + val container = mock[SampleJavaContainer] + + val spec = new TestSpec(assert(cond = false), SampleContainer(container)) + run(spec) + + verify(container).beforeTest(any()) + verify(container).start() + verify(container).afterTest(any(), ArgumentMatchers.eq(Optional.empty())) + verify(container).stop() + } + + test("start and stop container only once") { + val container = mock[SampleJavaContainer] + + val spec = new MultipleTestsSpec(assert(cond = true), SampleContainer(container)) + run(spec) + + verify(container, times(2)).beforeTest(any()) + verify(container).start() + verify(container, times(2)).afterTest(any(), any()) + verify(container).stop() + } + +} + +object ForAllTestContainersFixtureSpec { + trait ContainerSpec extends FunSuite with TestContainersFixtures + + protected class TestSpec(testBody: => Unit, container: Container) extends ContainerSpec { + val containerFixture = ForAllContainerFixture(container) + override def munitFixtures: Seq[Fixture[_]] = List(containerFixture) + + test("test") {testBody} + } + + protected class MultipleTestsSpec(testBody: => Unit, container: Container) extends ContainerSpec { + val containerFixture = ForAllContainerFixture(container) + override def munitFixtures: Seq[Fixture[_]] = List(containerFixture) + + test("test1") {testBody} + test("test2") {testBody} + } + + private def run(res: Suite): Unit = { + val notifier = new RunNotifier() + new MUnitRunner(res.asInstanceOf[Suite].getClass, () => res).run(notifier) + } +} diff --git a/test-framework/munit/src/test/scala/com/dimafeng/testcontainers/munit/fixtures/ForEachTestContainersFixtureSpec.scala b/test-framework/munit/src/test/scala/com/dimafeng/testcontainers/munit/fixtures/ForEachTestContainersFixtureSpec.scala new file mode 100644 index 00000000..f31961b6 --- /dev/null +++ b/test-framework/munit/src/test/scala/com/dimafeng/testcontainers/munit/fixtures/ForEachTestContainersFixtureSpec.scala @@ -0,0 +1,81 @@ +package com.dimafeng.testcontainers.munit.fixtures + +import java.util.Optional + +import com.dimafeng.testcontainers.ContainerDef +import com.dimafeng.testcontainers.munit.SampleContainer +import com.dimafeng.testcontainers.munit.SampleContainer.SampleJavaContainer +import com.dimafeng.testcontainers.munit.fixtures.ForEachTestContainersFixtureSpec._ +import munit.{FunSuite, MUnitRunner, Suite} +import org.junit.runner.notification.RunNotifier +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.{times, verify} +import org.mockito.{ArgumentMatchers, Mockito} +import org.scalatestplus.mockito.MockitoSugar +import com.dimafeng.testcontainers.Container + +class ForEachTestContainersFixtureSpec extends FunSuite with MockitoSugar { + test("call all appropriate methods of the containers") { + val container = mock[SampleJavaContainer] + + val spec = new TestSpec(assert(cond = true), SampleContainer(container)) + run(spec) + + verify(container).beforeTest(any()) + verify(container).start() + verify(container).afterTest(any(), ArgumentMatchers.eq(Optional.empty())) + verify(container).stop() + } + + test("call all appropriate methods of the containers if assertion fails") { + val container = mock[SampleJavaContainer] + + val spec = new TestSpec(assert(cond = false), SampleContainer(container)) + + run(spec) + + verify(container).beforeTest(any()) + verify(container).start() + verify(container).afterTest(any(), ArgumentMatchers.eq(Optional.empty())) + verify(container).stop() + } + + test("start and stop containers before and after each test case") { + val container = mock[SampleJavaContainer] + + val spec = new MultipleTestsSpec(assert(cond = true), SampleContainer(container)) + run(spec) + + verify(container, times(2)).beforeTest(any()) + verify(container, times(2)).start() + verify(container, times(2)).afterTest(any(), any()) + verify(container, times(2)).stop() + } + +} + +object ForEachTestContainersFixtureSpec { + protected abstract class ContainerSpec extends FunSuite with TestContainersFixtures + + protected class TestSpec(testBody: => Unit, container: Container) extends ContainerSpec { + val containerFixture = ForEachContainerFixture(container) + + override def munitFixtures: Seq[Fixture[_]] = List(containerFixture) + + test("test") {testBody} + } + + protected class MultipleTestsSpec(testBody: => Unit, container: Container) extends ContainerSpec { + val containerFixture = ForEachContainerFixture(container) + + override def munitFixtures: Seq[Fixture[_]] = List(containerFixture) + + test("test1") {testBody} + test("test2") {testBody} + } + + private def run(res: Suite): Unit = { + val notifier = new RunNotifier() + new MUnitRunner(res.asInstanceOf[Suite].getClass, () => res).run(notifier) + } +} \ No newline at end of file