Usage
dockerenv was created to expose a handle on common docker containers so they can easily be started/stopped in code.
The typical use-case is to support real integration testing against real services rather than having to mock things out.
The main project gives you a DockerEnv interface for common containers (kafka, mysql, postgres, mongo, etc).
With that you can do things like:
dockerenv.mysql().start() // start mysql
dockerenv.mysql().stop() // stop mysql
dockerenv.mysql().bracket {
// start mysql if it wasn't already running, and stop it when this bracket closes if it was started
}
As the primary use-case is really for testing, you may want to include a dependency on the “test” artifact as well as the main one:
libraryDependencies += "com.github.aaronp" %% "dockerenv" % "latest version" % "test" classifier "tests"
libraryDependencies += "com.github.aaronp" %% "dockerenv" % "latest version" % "test"
In doing so this gives you access to the ‘BaseXXXSpec’ traits (e.g. BaseKafkaSpec) which ensures that kafka is started/stopped for those tests:
// the BaseXYZSpec classes ensure XYZ is started/stopped for your tests, and expose a 'dockerHandle' should you need to
// stop the services for run more complex scenarios, like testing failover/retries
class MyKafkaTest extends BaseKafkaSpec {
"Kafka" should {
"connect to a running kafka instance" in {
isDockerRunning() shouldBe true
val topic = "testTopic"
// insert your code here which needs to do something with kafka (e.g. publish/consume some data)
// here we just execute a script within the kafka container to demonstrate it's running by invoking
// 'kafka-topics.sh' script within the container via our listTopics.sh wrapper
val Success((0, listOutput)) = dockerHandle.runInScriptDir("listTopics.sh")
listOutput should not be(empty)
}
}
}
NOTE:
As we’re starting/stopping external resources, you will want to ensure these sorts of tests are NOT run in parallel!
You could tag them as integration tests, or just put your integration tests in a separate test module.
You don’t need to use those ScalaTest convenience wrappers, however. Just depending on dockerenv gives you access to the currently supported containers. You could for example run a DB migration application locally like this:
println(step("Let's do some ETL! Starting Mysql...", 0))
dockerenv.mysql().bracket {
println(step("Mysql Started, Starting Kafka", 1))
dockerenv.kafka().bracket {
println(step("Kafka Started, Starting Mongo", 2))
dockerenv.mongo().bracket {
println(step("All up! Killing Mongo...", 3))
}
println(step("Mongo Down, Killing Kafka", 2))
}
println(step("Kafka Down, Killing Mysql", 1))
}
println(step("All done!", 0))
// pretty-prints the message with the current 'docker ps' status
private def step(msg: String, indent: Int): String = {
val status = dockerPS
...
}
def dockerPS: String = "docker ps".!!
The output for which is:
_____________________________________________________________________________________________________________________________
Let's do some ETL! Starting Mysql...
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
________________________________________________________________________________________________________________________________________________________________
Mysql Started, Starting Kafka
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
39d16d02b95c mysql:8.0.18 "docker-entrypoint.s…" 1 second ago Up Less than a second 33060/tcp, 0.0.0.0:7777->3306/tcp dockerenv-mysql
__________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
Kafka Started, Starting Mongo
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a6e9bea0393c dockerenv-kafka:test "supervisord -n" 1 second ago Up Less than a second 0.0.0.0:2181->2181/tcp, 0.0.0.0:8083->8083/tcp, 0.0.0.0:9080->9080/tcp, 0.0.0.0:9092->9092/tcp, 0.0.0.0:29092->29092/tcp test-kafka
39d16d02b95c mysql:8.0.18 "docker-entrypoint.s…" 2 seconds ago Up 1 second 33060/tcp, 0.0.0.0:7777->3306/tcp dockerenv-mysql
__________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
All up! Killing Mongo...
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
fef2d8c0662a mongo:4.0 "docker-entrypoint.s…" 1 second ago Up Less than a second 0.0.0.0:9010->27017/tcp dockerenv-mongo
a6e9bea0393c dockerenv-kafka:test "supervisord -n" 2 seconds ago Up 1 second 0.0.0.0:2181->2181/tcp, 0.0.0.0:8083->8083/tcp, 0.0.0.0:9080->9080/tcp, 0.0.0.0:9092->9092/tcp, 0.0.0.0:29092->29092/tcp test-kafka
39d16d02b95c mysql:8.0.18 "docker-entrypoint.s…" 3 seconds ago Up 2 seconds 33060/tcp, 0.0.0.0:7777->3306/tcp dockerenv-mysql
______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
Mongo Down, Killing Kafka
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a6e9bea0393c dockerenv-kafka:test "supervisord -n" 3 seconds ago Up 1 second 0.0.0.0:2181->2181/tcp, 0.0.0.0:8083->8083/tcp, 0.0.0.0:9080->9080/tcp, 0.0.0.0:9092->9092/tcp, 0.0.0.0:29092->29092/tcp test-kafka
39d16d02b95c mysql:8.0.18 "docker-entrypoint.s…" 4 seconds ago Up 3 seconds 33060/tcp, 0.0.0.0:7777->3306/tcp dockerenv-mysql
____________________________________________________________________________________________________________________________________________________________
Kafka Down, Killing Postgres
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
39d16d02b95c mysql:8.0.18 "docker-entrypoint.s…" 6 seconds ago Up 4 seconds 33060/tcp, 0.0.0.0:7777->3306/tcp dockerenv-mysql
_____________________________________________________________________________________________________________________________
All done!
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Under the hood this just issues CLI commands to docker, the output of which you can see by changing the script logging:
// get the 'mysql' implementation which logs the script output to standard out:
val mysql = dockerEnv.mysql().withLogger(dockerEnv.stdOut)
mysql.start()
mysql.stop()
mysql.bracket {
// mysql is running here
}