Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Sergei Egorov

May 17, 2019
Tweet

More Decks by Sergei Egorov

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. 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
  4. Mocking Local DBs VMs
 (Vagrant) Docker Fig
 (aka Docker Compose)

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

    Docker API Integration testing transformation @bsideup
  6. 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
  7. Declarative YAML redis:
 image: redis
 ports:
 - "6379:6379"
 postgres:
 image:

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

    postgres
 ports:
 - "5432:5432"
 elasticsearch:
 image: elasticsearch:5.0.0
 ports:
 - "9200:9200"
 @bsideup
  9. 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
  10. As simple as PostgreSQLContainer postgresql = new PostgreSQLContainer()
 
 GenericContainer

    redis = new GenericContainer("redis:3")
 .withExposedPorts(6379) @bsideup
  11. 1.6.x • Kafka module Jan, 2018 @bsideup try (KafkaContainer kafka

    = new KafkaContainer()) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); }
  12. 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>
  13. 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) );
  14. 1.7.x • Maven BOM • DockerCompose
 wait strategies • Daemon

    threads Apr, 2018 @bsideup kiraThread.setDaemon(true); kiraThread.start();
  15. 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)); // ... }
  16. 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 ) {} }
  17. 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" } }
  18. 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 )
  19. 1.8.x • OkHttp transport • Test framework
 agnostic • Docker

    cred. helpers • copyFileToContainer • Pulsar module • Couchbase module • Cassandra module Jun, 2018 @bsideup
  20. 1.9.x • OkHttp by default • Windows npipe support •

    Registry auth on Windows Sep, 2018 @bsideup
  21. 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);
  22. 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!")); }
  23. 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
  24. 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
  25. 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
  26. 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()); } }
  27. 1.10.x • JUnit 5 support • New docs & more

    examples • Env var to turn off Ryuk
 (for public CIs) Nov, 2018 @bsideup
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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"); }
  33. 1.11.x • Chaos Testing (toxiproxy) • fsync=off for PostgreSQL •

    Drop Netty transport • Rework shading Mar, 2019 @bsideup
  34. 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
  35. 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
  36. ❌ Hard to maintain ❌ Does not work with Kotlin/Scala

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

    ❌ Externally mutable objects ❌ “setX” isn’t supported, only “withX” (think collections) @bsideup
  38. ❌ 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
  39. KafkaContainer kafka = new KafkaContainer() { @Override protected void initialize()

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

    { withLogConsumer(new Slf4jLogConsumer(log)); withEmbeddedZookeeper(); withEnv("FOO", "BAR"); withStartupAttempts(5); } }; New “DSL”? Or even… @bsideup
  41. 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
  42. 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
  43. ✓ Super easy to maintain ✓ Works with any JVM

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

    language ✓ “Controllable mutability” - no modifications outside of “initialize” ✓ Void-retuning “setX” can easily be used @bsideup
  45. ✓ 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
  46. @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
  47. @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)));
  48. Testcontainers 1.x architecture Core
 (Docker, GenericContainer, 
 JUnit 4 int,

    test lifecycle) JUnit Jupiter int. Modules like MySQL/Kafka/… @bsideup
  49. 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
  50. 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
  51. ✓ Ultra-fast ITDD (Integration Test Driven Development) ✓ Minimal effort

    for the users ✓ Works for most of the containers @bsideup
  52. ✓ 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
  53. ✓ 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
  54. 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