com.github.j5ik2o:docker-controller-scala-core_2.13

docker-controller-scala-core


Keywords
docker-container, docker-java, scalatest
License
MIT

Documentation

docker-controller-scala

Actions Status: CI Scala Steward badge Maven Central License FOSSA Status

This library provides an easy and simple way to handle Docker Container or Docker Compose on ScalaTest, based on docker-java. The implementation of this library is thin, and if you know docker-java, your learning cost will be negligible.

Installation

Add the following to your sbt build (2.12.x, 2.13.x, 3.0.x):

val version = "..."

libraryDependencies += Seq(
  "com.github.j5ik2o" %% "docker-controller-scala-core" % version,
  "com.github.j5ik2o" %% "docker-controller-scala-scalatest" % version, // for scalatest
  // RDB
  "com.github.j5ik2o" %% "docker-controller-scala-mysql" % version, // optional
  "com.github.j5ik2o" %% "docker-controller-scala-postgresql" % version, // optional
  "com.github.j5ik2o" %% "docker-controller-scala-flyway" % version, // optional
  // NoSQL
  "com.github.j5ik2o" %% "docker-controller-scala-memcached" % version, // optional
  "com.github.j5ik2o" %% "docker-controller-scala-redis" % version, // optional
  "com.github.j5ik2o" %% "docker-controller-scala-elasticsearch" % version, // optional
  // Kafka
  "com.github.j5ik2o" %% "docker-controller-scala-zookeeper" % version, // optional
  "com.github.j5ik2o" %% "docker-controller-scala-kafka" % version, // optional
  // AWS Services
  "com.github.j5ik2o" %% "docker-controller-scala-dynamodb-local" % version, // optional
  "com.github.j5ik2o" %% "docker-controller-scala-minio" % version, // optional
  "com.github.j5ik2o" %% "docker-controller-scala-localstack" % version, // optional
  "com.github.j5ik2o" %% "docker-controller-scala-elasticmq" % version, // optional
)

In most cases, you can just select the scalatest module and the module you need.

libraryDependencies += Seq(
  "com.github.j5ik2o" %% "docker-controller-scala-scalatest" % version,
  "com.github.j5ik2o" %% "docker-controller-scala-mysql" % version,
)

Usage

DockerController that the thin wrapper for docker-java controls Docker Image and Docker Container for testing.

How to test with preset DockerController

The DockerController for the corresponding preset is as follows. Please see the corresponding **Spec for specific usage.

class MySQLControllerSpec extends AnyFreeSpec with DockerControllerSpecSupport {

  val hostPort: Int               = temporaryServerPort()
  val rootPassword: String        = "test"
  val dbName                      = "test"
  
  // MySQL
  val controller: MySQLController = MySQLController(dockerClient)(hostPort, rootPassword, databaseName = Some(dbName))

  // Specify DockerControllers to be launched.
  override protected val dockerControllers: Vector[DockerController] = Vector(controller)

  // Set the condition to wait for the container to be started.
  override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] =
    Map(
      controller -> WaitPredicateSetting(
        Duration.Inf,
        WaitPredicates.forListeningHostTcpPort(
          dockerHost,
          hostPort,
          1.seconds,
          Some(3.seconds)
        )
      )
    )

  "MySQLController" - {
    "run" in {
      var conn: Connection     = null
      var stmt: Statement      = null
      var resultSet: ResultSet = null
      try {
        Class.forName("com.mysql.cj.jdbc.Driver")
        conn = DriverManager.getConnection(
          s"jdbc:mysql://$dockerHost:$hostPort/$dbName?user=root&password=$rootPassword"
        )
        stmt = conn.createStatement
        resultSet = stmt.executeQuery("SELECT 1 FROM DUAL")
        while (resultSet.next())
          assert(resultSet.getInt(1) == 1)
      } catch {
        case NonFatal(ex) =>
          fail("occurred error", ex)
      } finally {
        if (resultSet != null)
          resultSet.close()
        if (stmt != null)
          stmt.close()
        if (conn != null)
          conn.close()
      }
    }
  }
}

Use Flyway Migrate Command on MySQL/PostgreSQL

If you'd like to use flyway module, you can use docker-controller-scala-flyway.

libraryDependencies += Seq(
  "com.github.j5ik2o" %% "docker-controller-scala-scalatest" % version,
  "com.github.j5ik2o" %% "docker-controller-scala-mysql" % version,
  "com.github.j5ik2o" %% "docker-controller-scala-flyway" % version, // for flyway
)

Mix-in FlywaySpecSupport then, put the sql files in src/reosources/flyway(src/reosources/** can be set to any string.), run flywayContext.flyway.migrate() in afterStartContainers method.

class MySQLControllerSpec extends AnyFreeSpec with DockerControllerSpecSupport with FlywaySpecSupport {
  val testTimeFactor: Int = sys.env.getOrElse("TEST_TIME_FACTOR", "1").toInt
  logger.debug(s"testTimeFactor = $testTimeFactor")

  val hostPort: Int        = temporaryServerPort()
  val dbName: String.      = "test"
  val rootUserName: String = "root"
  val rootPassword: String = "test"

  override protected def flywayDriverClassName: String = classOf[com.mysql.cj.jdbc.Driver].getName
  override protected def flywayDbHost: String          = dockerHost
  override protected def flywayDbHostPort: Int         = hostPort
  override protected def flywayDbName: String          = dbName
  override protected def flywayDbUserName: String      = rootUserName
  override protected def flywayDbPassword: String      = rootPassword

  override protected def flywayJDBCUrl: String =
    s"jdbc:mysql://$flywayDbHost:$flywayDbHostPort/$flywayDbName?useSSL=false&user=$flywayDbUserName&password=$flywayDbPassword"

  val controller: MySQLController = MySQLController(dockerClient)(hostPort, rootPassword, databaseName = Some(dbName))
  
  override protected val dockerControllers: Vector[DockerController] = Vector(controller)

  override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] =
    Map(
      controller -> WaitPredicateSetting(
        Duration.Inf,
        WaitPredicates.forListeningHostTcpPort(
          dockerHost,
          hostPort,
          (1 * testTimeFactor).seconds,
          Some((5 * testTimeFactor).seconds)
        )
      )
    )
  
  override protected def afterStartContainers(): Unit = {
    // Configure the sql files in `src/reosources/flyway`.
    val flywayContext = createFlywayContext(FlywayConfig(Seq("flyway")))
    // Execute flywayMigrate command
    flywayContext.flyway.migrate()
  }

  "MySQLController" - {
    "run" in {
      var conn: Connection     = null
      var stmt: Statement      = null
      var resultSet: ResultSet = null
      try {
        Class.forName(flywayDriverClassName)
        conn = DriverManager.getConnection(flywayJDBCUrl)
        stmt = conn.createStatement
        val result = stmt.executeUpdate("INSERT INTO users VALUES(1, 'kato')")
        assert(result == 1)
        resultSet = stmt.executeQuery("SELECT * FROM users")
        while (resultSet.next()) {
          val id   = resultSet.getInt("id")
          val name = resultSet.getString("name")
          println(s"id = $id, name = $name")
        }
      } catch {
        case NonFatal(ex) =>
          ex.printStackTrace()
          fail("occurred error", ex)
      } finally {
        if (resultSet != null)
          resultSet.close()
        if (stmt != null)
          stmt.close()
        if (conn != null)
          conn.close()
      }
    }
  }

}

How to test with DockerController your customized

To launch a docker container for testing

// In ScalaTest, please mix-in DockerControllerSpecSupport.
class NginxSpec extends AnyFreeSpec with DockerControllerSpecSupport {
  
  // choose whether to create and destroy containers per test class (ForAllTest) or per test (ForEachTest).
  override def createRemoveLifecycle: DockerContainerCreateRemoveLifecycle.Value =
    DockerContainerCreateRemoveLifecycle.ForEachTest

  // choose whether to start and stop containers per test class (ForAllTest) or per test (ForEachTest).
  override def startStopLifecycle: DockerContainerStartStopLifecycle.Value =
    DockerContainerStartStopLifecycle.ForEachTest
    
  val nginx: DockerController = DockerController(dockerClient)(
    imageName = "nginx",
    tag = Some("latest")
  ).configureCreateContainerCmd { cmd =>
    // if customize the container generation, please do the following.
    // In this example, a random host port is specified.
    val hostPort: Int              = temporaryServerPort()
    val containerPort: ExposedPort = ExposedPort.tcp(80)
    val portBinding: Ports         = new Ports()
    portBinding.bind(containerPort, Ports.Binding.bindPort(hostPort))
    logger.debug(s"hostPort = $hostPort, containerPort = $containerPort")
    cmd
      .withExposedPorts(containerPort)
      .withHostConfig(newHostConfig().withPortBindings(portBinding))
  }

  // Specify DockerControllers to be launched.
  override protected val dockerControllers: Vector[DockerController] = {
    Vector(nginx)
  }

  // Set the condition to wait for the container to be started.
  override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] =
    Map(
      nginx -> WaitPredicateSetting(
        Duration.Inf,
        WaitPredicates.forLogMessageContained("Configuration complete; ready for start up")
      )
    )

  "nginx" - {
    "run-1" in {
      val hostPort = nginx.inspectContainer().getNetworkSettings.bindingHostPort(ExposedPort.tcp(80)).get
      val url      = new URL(s"http://$dockerHost:$hostPort")
      HttpRequestUtil.wget(url)
    }
    "run-2" in {
      val hostPort = nginx.inspectContainer().getNetworkSettings.bindingHostPort(ExposedPort.tcp(80)).get
      val url      = new URL(s"http://$dockerHost:$hostPort")
      HttpRequestUtil.wget(url)
    }
  }
}

How to use Docker Compose

  • Place the docker-compose.yml.ftl(ftl is Freemarker template) in src/test/resources. docker-compose.yml.ftl can be renamed to anything you want.
  • The variables in the ftl can be freely determined.
version: '3'
services:
  nginx:
    image: nginx
    ports:
      - ${nginxHostPort}:80
  • Use DockerComposeController, which is a subtype of DockerController. Other than this, it is the same as the test method above.
  • Pass the context containing the values of the variables to be used in the FTL to the constructor of DockerComposeController.
class NginxSpec extends AnyFreeSpec with DockerControllerSpecSupport {
// ...
  val buildDir: File                = ResourceUtil.getBuildDir(getClass)
  val dockerComposeWorkingDir: File = new File(buildDir, "docker-compose")
  val dockerController = DockerComposeController(dockerClient)(
    dockerComposeWorkingDir,
    "docker-compose.yml.ftl",
    Map("nginxHostPort" -> hostPort.toString)
  )

  override val dockerControllers: Vector[DockerController] = {
    Vector(dockerController)
  }
// ...
}     

License

FOSSA Status