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,275