GeeCON 2019: Testcontainers: a year-in-review

GeeCON 2019: Testcontainers: a year-in-review

Unit testing is fine, but without proper integration testing, especially if you work with external resources like databases and other services, you might not know how your application will actually behave once it has been deployed to the real production environment.

Testcontainers is a popular JVM testing library that provides lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.

In this talk, we will briefly go throught the past, present and the future of the library.

3329a8ef50ae3388665964637001fcb6?s=128

Sergei Egorov

May 17, 2019
Tweet

Transcript

  1. Testcontainers:
 a year-in-review Sergei Egorov Kraków, 15-17 May 2019 @bsideup

  2. Top ques!ons (0) Room 11 Join at slido.com #geecon2019 Join

    at #geecon2019 Room 11 slido com Ask questions at
 slido.com
 #geecon2019
 room 1
  3. Tweet photos
 #geecon
 @bsideup

  4. About me • Testcontainers co-maintainer • Staff Engineer at Pivotal’s

    Spring R&D, working on Project Reactor ⚛ • Berlin Spring User Group co-organizer • Developer tools geek @bsideup
  5. Integration testing Why it matters?

  6. @bsideup

  7. @bsideup

  8. Integration testing Real-world, but isolated testing Spot the issues before

    the real environment Can be run during the development You have to start real databases Should be cross-platform Slower than Unit testing Pros Cons @bsideup
  9. Integration testing transformation @bsideup

  10. Mocking Integration testing transformation @bsideup

  11. Mocking Local DBs Integration testing transformation @bsideup

  12. Mocking Local DBs VMs
 (Vagrant) Integration testing transformation @bsideup

  13. Mocking Local DBs VMs
 (Vagrant) Docker Integration testing transformation @bsideup

  14. Mocking Local DBs VMs
 (Vagrant) Docker Fig
 (aka Docker Compose)

    Integration testing transformation @bsideup
  15. Mocking Local DBs VMs
 (Vagrant) Docker Fig
 (aka Docker Compose)

    Docker API Integration testing transformation @bsideup
  16. None
  17. Abstraction layer

  18. CI friendly

  19. Cross-platform

  20. Docker Compose FTW! redis:
 image: redis
 ports:
 - "6379:6379"
 postgres:


    image: postgres
 ports:
 - "5432:5432"
 elasticsearch:
 image: elasticsearch:5.0.0
 ports:
 - "9200:9200"
 @bsideup
  21. But…

  22. Declarative YAML redis:
 image: redis
 ports:
 - "6379:6379"
 postgres:
 image:

    postgres
 ports:
 - "5432:5432"
 elasticsearch:
 image: elasticsearch:5.0.0
 ports:
 - "9200:9200"
 @bsideup
  23. ports randomization? redis:
 image: redis
 ports:
 - "6379:6379"
 postgres:
 image:

    postgres
 ports:
 - "5432:5432"
 elasticsearch:
 image: elasticsearch:5.0.0
 ports:
 - "9200:9200"
 @bsideup
  24. Container per test? redis:
 image: redis
 ports:
 - "6379:6379"
 postgres:


    image: postgres
 ports:
 - "5432:5432"
 elasticsearch:
 image: elasticsearch:5.0.0
 ports:
 - "9200:9200"
 @bsideup
  25. IDE integration? @bsideup

  26. Fighting with Docker environment

  27. There is no place like

  28. There is no place like … unless there is

  29. Can we improve that?

  30. None
  31. Testcontainers • http://github.com/testcontainers/testcontainers-java • Wraps docker-java library • Docker environment

    discovery (Win, Mac, Linux) • Containers cleanup on JVM shutdown @bsideup
  32. As simple as PostgreSQLContainer postgresql = new PostgreSQLContainer()
 
 GenericContainer

    redis = new GenericContainer("redis:3")
 .withExposedPorts(6379) @bsideup
  33. Users

  34. @bsideup

  35. Demo

  36. 1.6.x Jan, 2018 @bsideup

  37. 1.6.x • Kafka module Jan, 2018 @bsideup try (KafkaContainer kafka

    = new KafkaContainer()) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); }
  38. 1.6.x • Kafka module • “Ryuk” Jan, 2018 @bsideup

  39. 1.7.x Apr, 2018 @bsideup

  40. 1.7.x • Maven BOM Apr, 2018 @bsideup <dependencyManagement> <dependencies> <dependency>

    <groupId>org.testcontainers</groupId> <artifactId>testcontainers-bom</artifactId> <version>1.11.2</version> <type>bom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
  41. 1.7.x • Maven BOM • DockerCompose
 wait strategies Apr, 2018

    @bsideup new DockerComposeContainer(new File("docker-compose.yml")) .withExposedService( "redis_1", REDIS_PORT, Wait.forListeningPort() ) .withExposedService( "db_1", 3306, Wait.forLogMessage(".*ready for connections.*\\s", 1) );
  42. 1.7.x • Maven BOM • DockerCompose
 wait strategies • Daemon

    threads Apr, 2018 @bsideup kiraThread.setDaemon(true); kiraThread.start();
  43. 1.7.x • Maven BOM • DockerCompose
 wait strategies • Daemon

    threads • MockServer module Apr, 2018 @bsideup try (MockServerContainer mockServer = new MockServerContainer()) { mockServer.start(); String expectedBody = "Hello Default World!"; MockServerClient client = new MockServerClient( mockServer.getContainerIpAddress(), mockServer.getServerPort() ); client.when(request("/hello")).respond(response(expectedBody)); // ... }
  44. 1.8.x Jun, 2018 @bsideup

  45. 1.8.x • OkHttp transport Jun, 2018 @bsideup

  46. 1.8.x • OkHttp transport • Test framework
 agnostic Jun, 2018

    @bsideup public interface Startable extends AutoCloseable { void start(); void stop(); } public interface TestLifecycleAware { default void beforeTest(TestDescription description) {} default void afterTest( TestDescription description, Optional<Throwable> throwable ) {} }
  47. 1.8.x • OkHttp transport • Test framework
 agnostic • Docker

    cred. helpers Jun, 2018 @bsideup { "auths": { }, "HttpHeaders": { "User-Agent": "Docker-Client/18.03.0-ce (darwin)" }, "credHelpers": { "registry.example.com": “helper" } }
  48. 1.8.x • OkHttp transport • Test framework
 agnostic • Docker

    cred. helpers • copyFileToContainer Jun, 2018 @bsideup GenericContainer container = new GenericContainer("alpine:latest") // Look, Ma! No volumes mounting! .withCopyFileToContainer( MountableFile.forClasspathResource(“/mappable-resource/"), containerPath )
  49. 1.8.x • OkHttp transport • Test framework
 agnostic • Docker

    cred. helpers • copyFileToContainer • Pulsar module • Couchbase module • Cassandra module Jun, 2018 @bsideup
  50. 1.9.x Sep, 2018 @bsideup

  51. 1.9.x • OkHttp by default Sep, 2018 @bsideup

  52. 1.9.x • OkHttp by default • Windows npipe support Sep,

    2018 @bsideup No longer needed
  53. 1.9.x • OkHttp by default • Windows npipe support •

    Registry auth on Windows Sep, 2018 @bsideup
  54. 1.9.x • OkHttp by default • Windows npipe support •

    Registry auth on Windows • Fix local Docker Compose
 on Windows Sep, 2018 @bsideup new DockerComposeContainer(new File("docker-compose.yml")) .withExposedService("redis_1", REDIS_PORT) .withExposedService("db_1", 3306) .withLocalCompose(true);
  55. 1.9.x • OkHttp by default • Windows npipe support •

    Registry auth on Windows • Fix local Docker Compose
 on Windows • Host ports exposing Sep, 2018 @bsideup @BeforeClass public static void setUp() { localPort = server.getAddress().getPort(); Testcontainers.exposeHostPorts(localPort); } @Rule public BrowserWebDriverContainer browser = new BrowserWebDriverContainer() .withCapabilities(new ChromeOptions()); @Test public void testContainerRunningAgainstExposedHostPort() { RemoteWebDriver webDriver = browser.getWebDriver(); webDriver.get( String.format("http://host.testcontainers.internal:%d/", localPort)); final String pageSource = webDriver.getPageSource(); assertTrue(pageSource.contains("Hello from the host!")); }
  56. 1.9.x • OkHttp by default • Windows npipe support •

    Registry auth on Windows • Fix local Docker Compose
 on Windows • Host ports exposing • Random ports in Couchbase Sep, 2018 @bsideup
  57. 1.9.x • OkHttp by default • Windows npipe support •

    Registry auth on Windows • Fix local Docker Compose
 on Windows • Host ports exposing • Random ports in Couchbase Sep, 2018 @bsideup
  58. 1.9.x • OkHttp by default • Windows npipe support •

    Registry auth on Windows • Fix local Docker Compose
 on Windows • Host ports exposing • Random ports in Couchbase • ClickHouse and Postgis modules Sep, 2018 @bsideup
  59. 1.10.x Nov, 2018 @bsideup

  60. 1.10.x • JUnit 5 support Nov, 2018 @bsideup @Testcontainers class

    MyTestcontainersTests { // will be shared between test methods @Container static final MySQLContainer MYSQL_CONTAINER = new MySQLContainer(); // will be started before and stopped after each test method @Container PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer() .withDatabaseName("foo") .withUsername("foo") .withPassword(“secret"); @Test void test() { assertTrue(MYSQL_CONTAINER.isRunning()); assertTrue(postgresqlContainer.isRunning()); } }
  61. 1.10.x • JUnit 5 support • New docs & more

    examples Nov, 2018 @bsideup
  62. 1.10.x • JUnit 5 support • New docs & more

    examples • Env var to turn off Ryuk
 (for public CIs) Nov, 2018 @bsideup
  63. 1.10.x • JUnit 5 support • New docs & more

    examples • Env var to turn off Ryuk
 (for public CIs) • shm + TmpFS settings Nov, 2018 @bsideup
  64. 1.10.x • JUnit 5 support • New docs & more

    examples • Env var to turn off Ryuk
 (for public CIs) • shm + TmpFS settings • Auto dependency updates
 with Dependabot Nov, 2018 @bsideup
  65. 1.10.x • JUnit 5 support • New docs & more

    examples • Env var to turn off Ryuk
 (for public CIs) • shm + TmpFS settings • Auto dependency updates
 with Dependabot Nov, 2018 @bsideup
  66. 1.10.x • JUnit 5 support • New docs & more

    examples • Env var to turn off Ryuk
 (for public CIs) • shm + TmpFS settings • Auto dependency updates
 with Dependabot • Neo4j and Elasticsearch modules Nov, 2018 @bsideup
  67. 1.11.x Mar, 2019 @bsideup

  68. 1.11.x • Chaos Testing (toxiproxy) Mar, 2019 @bsideup @Rule public

    GenericContainer redis = new GenericContainer("redis:5.0.4") .withExposedPorts(6379) .withNetwork(network); @Rule public ToxiproxyContainer toxiproxy = new ToxiproxyContainer() .withNetwork(network); @Test public void testLatencyViaProxy() throws IOException { ContainerProxy proxy = toxiproxy.getProxy(redis, 6379); Jedis jedis = new Jedis( proxy.getContainerIpAddress(), proxy.getProxyPort() ); proxy.toxics() .latency("latency", ToxicDirection.DOWNSTREAM, 1_100) .setJitter(100); jedis.get("somekey"); }
  69. 1.11.x • Chaos Testing (toxiproxy) • fsync=off for PostgreSQL Mar,

    2019 @bsideup
  70. 1.11.x • Chaos Testing (toxiproxy) • fsync=off for PostgreSQL •

    Drop Netty transport Mar, 2019 @bsideup
  71. 1.11.x • Chaos Testing (toxiproxy) • fsync=off for PostgreSQL •

    Drop Netty transport Mar, 2019 @bsideup
  72. 1.11.x • Chaos Testing (toxiproxy) • fsync=off for PostgreSQL •

    Drop Netty transport • Rework shading Mar, 2019 @bsideup
  73. Roadmap

  74. new DSL

  75. What’s wrong with the current one?

  76. KafkaContainer kafka = new KafkaContainer() .withLogConsumer(new Slf4jLogConsumer(log)) .withEmbeddedZookeeper() .withEnv("FOO", "BAR")

    .withStartupAttempts(5); Current DSL @bsideup
  77. KafkaContainer kafka = new KafkaContainer() .withLogConsumer(new Slf4jLogConsumer(log)) .withEmbeddedZookeeper() .withEnv("FOO", "BAR")

    .withStartupAttempts(5); Current DSL GenericContainer @bsideup
  78. KafkaContainer kafka = new KafkaContainer() .withLogConsumer(new Slf4jLogConsumer(log)) .withEmbeddedZookeeper() .withEnv("FOO", "BAR")

    .withStartupAttempts(5); Current DSL KafkaContainer @bsideup
  79. KafkaContainer kafka = new KafkaContainer() .withLogConsumer(new Slf4jLogConsumer(log)) .withEmbeddedZookeeper() .withEnv("FOO", "BAR")

    .withStartupAttempts(5); Current DSL GenericContainer @bsideup
  80. KafkaContainer kafka = new KafkaContainer() .withLogConsumer(new Slf4jLogConsumer(log)) .withEmbeddedZookeeper() .withEnv("FOO", "BAR")

    .withStartupAttempts(5); Current DSL GenericContainer @bsideup
  81. KafkaContainer kafka = new KafkaContainer() .withLogConsumer(new Slf4jLogConsumer(log)) .withEmbeddedZookeeper() .withEnv("FOO", "BAR")

    .withStartupAttempts(5); Current DSL GenericContainer public SELF withStartupAttempts(int attempts) { this.startupAttempts = attempts; return self(); } @bsideup
  82. public class KafkaContainer extends GenericContainer<KafkaContainer> {} public class GenericContainer<SELF extends

    GenericContainer<SELF>> /* */ {} @bsideup
  83. public class KafkaContainer extends GenericContainer<KafkaContainer> {} public class GenericContainer<SELF extends

    GenericContainer<SELF>> /* */ {} https://youtrack.jetbrains.com/issue/KT-17186 https://stackoverflow.com/questions/39163749/how-to-use-a-java-self-bounded-class-in-scala @bsideup
  84. ❌ Hard to maintain @bsideup

  85. ❌ Hard to maintain ❌ Does not work with Kotlin/Scala

    @bsideup
  86. ❌ Hard to maintain ❌ Does not work with Kotlin/Scala

    ❌ Externally mutable objects @bsideup
  87. ❌ Hard to maintain ❌ Does not work with Kotlin/Scala

    ❌ Externally mutable objects ❌ “setX” isn’t supported, only “withX” (think collections) @bsideup
  88. ❌ Hard to maintain ❌ Does not work with Kotlin/Scala

    ❌ Externally mutable objects ❌ “setX” isn’t supported, only “withX” (think collections) ❌ No imperative “if-else” with the fluent style @bsideup
  89. KafkaContainer kafka = new KafkaContainer() { @Override protected void initialize()

    { withLogConsumer(new Slf4jLogConsumer(log)); withEmbeddedZookeeper(); withEnv("FOO", "BAR"); withStartupAttempts(5); } }; New “DSL”? @bsideup
  90. KafkaContainer kafka = new KafkaContainer() { @Override protected void initialize()

    { withLogConsumer(new Slf4jLogConsumer(log)); withEmbeddedZookeeper(); withEnv("FOO", "BAR"); withStartupAttempts(5); } }; New “DSL”? Or even… @bsideup
  91. KafkaContainer kafka = new KafkaContainer() {{ withLogConsumer(new Slf4jLogConsumer(log)); withEmbeddedZookeeper(); withEnv("FOO",

    "BAR"); withStartupAttempts(5); }} New “DSL”? @bsideup
  92. KafkaContainer kafka = new KafkaContainer() {{ withLogConsumer(new Slf4jLogConsumer(log)); withEmbeddedZookeeper(); withEnv("FOO",

    "BAR"); withStartupAttempts(5); }} New “DSL”? public void withStartupAttempts(int attempts) { this.startupAttempts = attempts; } @bsideup
  93. KafkaContainer kafka = new KafkaContainer() {{ withLogConsumer(new Slf4jLogConsumer(log)); withEmbeddedZookeeper(); withEnv("FOO",

    "BAR"); withStartupAttempts(5); }} New “DSL”? public void withStartupAttempts(int attempts) { this.startupAttempts = attempts; } @bsideup
  94. ✓ Super easy to maintain @bsideup

  95. ✓ Super easy to maintain ✓ Works with any JVM

    language @bsideup
  96. ✓ Super easy to maintain ✓ Works with any JVM

    language ✓ “Controllable mutability” - no modifications outside of “initialize” @bsideup
  97. ✓ Super easy to maintain ✓ Works with any JVM

    language ✓ “Controllable mutability” - no modifications outside of “initialize” ✓ Void-retuning “setX” can easily be used @bsideup
  98. ✓ Super easy to maintain ✓ Works with any JVM

    language ✓ “Controllable mutability” - no modifications outside of “initialize” ✓ Void-retuning “setX” can easily be used ✓ Can use “if-else” @bsideup
  99. GraalVM focus

  100. @bsideup https://medium.com/graalvm/using-testcontainers-from-a-node-js- application-3aa2273bf3bb

  101. @bsideup // JavaScript var GenericContainer = Java.type(’org.testcontainers.containers.GenericContainer’); var container =

    new GenericContainer("nginx"); container.setExposedPorts([80]); container.start(); console.log(container.getContainerIpAddress() + ‘:’ + container.getMappedPort(80)); https://medium.com/graalvm/using-testcontainers-from-a-node-js- application-3aa2273bf3bb
  102. @bsideup // JavaScript var GenericContainer = Java.type(’org.testcontainers.containers.GenericContainer’); var container =

    new GenericContainer("nginx"); container.setExposedPorts([80]); container.start(); console.log(container.getContainerIpAddress() + ‘:’ + container.getMappedPort(80)); https://medium.com/graalvm/using-testcontainers-from-a-node-js- application-3aa2273bf3bb // Python import java generic = java.type('org.testcontainers.containers.GenericContainer') container = generic('nginx') container.setExposedPorts([80]) container.start(); print('%s:%s' % (container.getContainerIpAddress(), container.getMappedPort(80)));
  103. “container core”

  104. Testcontainers 1.x architecture Core
 (Docker, GenericContainer, 
 JUnit 4 int,

    test lifecycle) @bsideup
  105. Testcontainers 1.x architecture Core
 (Docker, GenericContainer, 
 JUnit 4 int,

    test lifecycle) JUnit Jupiter int. @bsideup
  106. Testcontainers 1.x architecture Core
 (Docker, GenericContainer, 
 JUnit 4 int,

    test lifecycle) JUnit Jupiter int. Modules like MySQL/Kafka/… @bsideup
  107. Testcontainers 2.x architecture Container-core
 (GenericContainer) @bsideup

  108. Testcontainers 2.x architecture Container-core
 (GenericContainer) Test Frameworks JUnit 4 JUnit

    Jupiter Scala test? … @bsideup
  109. Testcontainers 2.x architecture Container-core
 (GenericContainer) Test Frameworks JUnit 4 JUnit

    Jupiter Scala test? … Executing engines Container-core-docker
 (env discovery, networks, …) Container-core-k8s? … @bsideup
  110. Modules like MySQL/Kafka/… Testcontainers 2.x architecture Container-core
 (GenericContainer) Test Frameworks

    JUnit 4 JUnit Jupiter Scala test? … Executing engines Container-core-docker
 (env discovery, networks, …) Container-core-k8s? … @bsideup
  111. None
  112. Usability Flexibility Speed Features @bsideup

  113. Usability Flexibility Speed Features @bsideup

  114. Usability Flexibility Speed Features @bsideup

  115. Please welcome…

  116. Reusable containers

  117. Reusable containers

  118. Demo

  119. @bsideup

  120. ✓ Ultra-fast ITDD (Integration Test Driven Development) @bsideup

  121. ✓ Ultra-fast ITDD (Integration Test Driven Development) ✓ Minimal effort

    for the users @bsideup
  122. ✓ Ultra-fast ITDD (Integration Test Driven Development) ✓ Minimal effort

    for the users ✓ Works for most of the containers @bsideup
  123. ✓ Ultra-fast ITDD (Integration Test Driven Development) ✓ Minimal effort

    for the users ✓ Works for most of the containers ✓ Eventually cleanups stale containers
 (unlike Docker Compose) @bsideup
  124. ✓ Ultra-fast ITDD (Integration Test Driven Development) ✓ Minimal effort

    for the users ✓ Works for most of the containers ✓ Eventually cleanups stale containers
 (unlike Docker Compose) ✓ Available later this year. Pre-order now for $0 @bsideup
  125. Takeaways • https://testcontainers.org • Works on Linux, Mac and Windows

    • …including CIs like Jenkins, Travis, CircleCI, BitBucket, AzurePipelines, … • Provides a great balance between 
 flexibility, usability, speed and features
 @bsideup
  126. @bsideup bsideup