Integration Testing with
Docker and Testcontainers
- Technical Lead @ G DATA Advanced Analytics
- 2019: Software Engineer @ codecentric
- Consultant @ Styrascosoft GbR
- Trainer @ bee42
- Testcontainers Maintainer and Open Source Enthusiast
- Oracle Developer Champion & Groundbreaker Amabassador
- Organizer Software Craftsmanship Meetup Ruhr
- FH-GE / WHS Alumni
Kevin Wittek @kiview
Key learnings
- Easy setup of complete test environments
- Not a lot effort to integration test using real external applications
- Development history of Docker Spock-Extension
$ git clone https://github.com/foo/bar.git
$ cd bar
$ ./gradlew build
My definiton
For example:
- Launch application server
- Framework/Library interactions
- Interact with external ports (e.g. network, file system, database)
"Tests which interact with external systems/dependencies"
And what about Microservices?
Testing Honeycomb
Integrated Tests
"I use the term integrated test to mean any test whose result (pass or fail) depends on the correctness of the implementation of more than one piece of non-trivial behavior." - J.B. Rainsberger
"A test that will pass or fail based on the correctness of another system." - Spotify
Integrated Tests
For example:
- We (manually) spin up other services in a local testing environment
- We test against other services in a shared testing environment
- Changes to your system breaks tests for other systems
Which tools to use?
(in the JVM world)
def "item from list is removable"() {
given: "a simple list"
def list = [1, 2, 3, 4]
when: "removing the first element"
list.remove(0)
then: "resulting list is tail of original list"
list == [2, 3, 4]
}
class HelloSpockSpec extends spock.lang.Specification {
def "length of Spock's and his friends' names"() {
expect:
name.size() == length
where:
name | length
"Spock" | 5
"Kirk" | 4
"Scotty" | 6
}
}
Why combine?
- Easy setup of dev environment
- Uniform build and test environments
- Also self contained and portable!
- No installation and setup of external software
- Except Docker ;)
$ git clone https://github.com/foo/bar.git
$ cd bar
$ ./gradlew build
How to fuse both worlds?
Spock + Docker = Spocker?
Or: How to build your own Spock extension
@Docker(image = "emilevauge/whoami", ports = @PortBinding("8080:80"))
class DockerExtensionIT extends Specification {
def "should start accessible docker container"() {
given: "a http client"
def client = HttpClientBuilder.create().build()
when: "accessing web server"
def response = client.execute(new HttpGet("http://localhost:8080"))
then: "docker container is running and returns http status code 200"
response.statusLine.statusCode == 200
}
}
First commit
class DockerMethodInterceptor extends AbstractMethodInterceptor {
private final DockerClientFacade dockerClient
DockerMethodInterceptor(Docker docker) {
this(new DockerClientFacade(docker))
}
DockerMethodInterceptor(DockerClientFacade dockerClient) {
this.dockerClient = dockerClient
}
@Override
void interceptSpecExecution(IMethodInvocation invocation) throws Throwable {
dockerClient.startContainer()
invocation.proceed()
dockerClient.stopContainer()
}
}
First commit
GenericContainer redis =
new GenericContainer("redis:3.0.2")
.withExposedPorts(6379);
redis.start();
// test my stuff
redis.stop();
- Generic Containers
- Specialized Containers (Databases, Selenium, Kafka)
- Dockerfile
- Dockerfile DSL
- Docker-Compose
- Dynamic port binding and API
- WaitStrategies
- Docker environment discovery (e.g. docker-machine, DOCKER_HOST, Docker for Mac, Docker for Windows)
- Platform independent
- Linux, macOS, Windows 10 (with NPIPE support!)
@ashleymcnamara
Works on Windows too!
Testcontainers 1.9.1
Testcontainers 1.10.1
JDK11 Compatible!
Annotations a good idea?
@Docker(image = "postgres", ports = ["5432:5432"], env = [
@Env(key = "POSTGRES_USER", value = "foo"),
@Env(key = "POSTGRES_PASSWORD", value = "secret")
],
waitStrategy = { new JdbcWaitStrategy(username: "foo",
password: "secret",
jdbcUrl: "jdbc:postgresql:foo",
driverClassName: "org.postgresql.Driver",
testQueryString: "SELECT 1") })
def "waits until postgres accepts jdbc connections"() {
// tests
}
Testcontainers all in!
@Testcontainers
class TestContainersClassIT extends Specification {
@Shared
GenericContainer genericContainer = new GenericContainer("postgres:latest")
.withExposedPorts(5432)
.withEnv([
POSTGRES_USER: "foo"
POSTGRES_PASSWORD: "secret"
])
}
// Set up a redis container
@ClassRule
public static GenericContainer redis =
new GenericContainer("redis:3.0.2")
.withExposedPorts(6379);
Testcontainers-Jupiter (JUnit5)
@Testcontainers
class SomeTest {
@Container
private MySQLContainer mySQLContainer = new MySQLContainer();
@Test
void someTestMethod() {
String url = mySQLContainer.getJdbcUrl();
// create a connection and run test as normal
}
}
Docker-Compose
version: "3"
services:
vote:
image: vote-frontend
command: python app.py
volumes:
- ./vote:/app
ports:
- "5000:80"
redis:
image: redis:alpine
ports: ["6379"]
worker:
image: worker
db:
image: postgres:9.4
result:
image: result-frontend
command: nodemon --debug server.js
volumes:
- ./result:/app
ports:
- "5001:80"
- "5858:5858"
class GebHomepageSpec extends GebSpec {
def "can access The Book of Geb via homepage"() {
when:
to GebHomePage
and:
highlights.jQueryLikeApi.click()
then:
sectionTitles == ["Navigating Content", "Form Control Shortcuts"]
highlights.jQueryLikeApi.selected
}
}
CI inside containers?
Docker in Docker - DinD
Docker Wormhole
Docker Wormhole
Future
- Testcontainers 2.0
- Container-Core
- Test-Framework agnostic
- OO abstraction of Docker containers
- Other languages
- .NET, Go, Rust and JavaScript already exist (moved to Testcontainers org on Github)
- GraalVM (mind=blown)
- Windows Containers on Windows (WCOW) support
- Build and test inside containers with full IDE support
Resources
17.12: IFIS Weihnachtsfeier
Integration Testing with Testcontainers (WHS)
By Kevin Wittek
Integration Testing with Testcontainers (WHS)
- 1,363