docker-controller-scala
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.
- RDBMS
- NoSQL
- AWS Storages
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) insrc/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 ofDockerController
. 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)
}
// ...
}