Integration Testing with

Docker and Testcontainers

 

  • Doctoral Researcher @ Institute for Internet Security
    • Research Group Lead Blockchain
  • Consultant @ Styrascosoft GbR
  • Testcontainers Maintainer and Open Source Enthusiast
  • Oracle Groundbreaker Ambassador
  • Gitkraken Ambassador
  • Organizer Software Craftsmanship Meetup Ruhr

Kevin Wittek          @kiview

Why do we Test?

Because else we might end up here!

Key learnings

  • Easy setup of complete test environments
  • Not a lot effort to integration test using real external applications
  • Avoid integrated tests!
$ 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"

But why a pyramid?

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 and beyond)

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 😜

How to fuse both worlds?

GenericContainer redis =
    new GenericContainer("redis:3.0.2")
               .withExposedPorts(6379);

redis.start();

// test my stuff

redis.stop();

The project

  • testcontainers-java first released in 2015
  • 100% OSS, MIT licensed
  • 65 releases, 149 contributors
  • Core maintainers
    • Richard North
    • Sergei Egorov
    • Kevin Wittek
  • Forks in Python, C#, Rust, Go, JS; Scala wrapper

Capabilities

  • Generic docker container support
    • use any Docker image to support tests
  • Databases (many!) and Stream processing (Kafka, Pulsar)
  • AWS mocks (Localstack)
  • Docker Compose
  • Selenium
  • Chaos testing (toxiproxy)

Capabilities (2)

  • 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!

JDK12 Compatible!
+ regularly build against openjdk-ea

// Set up a redis container
@ClassRule
public static GenericContainer redis =
    new GenericContainer("redis:3.0.2")
               .withExposedPorts(6379);

JUnit4 Rules

Spock Extension


@Testcontainers
class TestContainersClassIT extends Specification {


    @Shared
    GenericContainer genericContainer = 
       new GenericContainer("postgres:latest")
            .withExposedPorts(5432)
            .withEnv([
                POSTGRES_USER: "foo"
                POSTGRES_PASSWORD: "secret"
            ])
}

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

and Common Pitfalls...

SELF-typing is sometimes a PITA

  • Pattern defined in "Effective Java"
    • Generic type with a recursive type paramter
    • MyClass<T extends MyClass<T>>
  • Doesn't play nice with other JVM languages (Scala, Kotlin)
  • Confusing for new users and contributors
  • Not consistent with all modules

Ideas

  • Using functional Builder-Pattern
    • Maybe Lombok @SuperBuilder?
  • Double Brace Initialization
    • People don't like this 😱

Double Brace Initialization 😜

private GenericContainer myContainer = new GenericContainer("myImage:42.23") {{
   withExposedPorts(4711);

   if (System.getenv().get("INSIDE_CI") == null) {
       org.testcontainers.Testcontainers.exposeHostPorts(8080);
   }

}};

TDD workflows

  • Container startup introduces overhead
  • Testcontainers should be ephemeral
  • We still want to avoid test pollution and integrated tests

One more thing...

Welcome reusable containers!

Future (2)

  • Testcontainers 2.0
  • Container-Core
    • Test-Framework agnostic
    • OO abstraction of Docker containers
  • Other languages
    • More languages and aligning existing forks
    • GraalVM (mind=blown)

Far Future...

TODO: add Safe Harbor Statement here

  • Windows Containers on Windows (WCOW)
  • Orchestrators (Docker Swarm, Kubernetes)
    • Maybe one day... (Help from experts welcome!)
  • Other container engines (e.g. Podman, Firecracker)
  • Build and test inside containers with full IDE support
    • Cloud IDEs
    • Visual Studio Code Remote Development

Resources

Testing with Testcontainers (HSD)

By Kevin Wittek

Testing with Testcontainers (HSD)

  • 1,040