Slide 1

Slide 1 text

Testcontainers:
 a year-in-review Sergei Egorov @bsideup

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Integration testing Why it matters?

Slide 4

Slide 4 text

@bsideup

Slide 5

Slide 5 text

@bsideup

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Integration testing transformation @bsideup

Slide 8

Slide 8 text

Mocking Integration testing transformation @bsideup

Slide 9

Slide 9 text

Mocking Local DBs Integration testing transformation @bsideup

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

Abstraction layer

Slide 16

Slide 16 text

CI friendly

Slide 17

Slide 17 text

Cross-platform

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

But…

Slide 20

Slide 20 text

Declarative YAML redis: image: redis ports: - "6379:6379" postgres: image: postgres ports: - "5432:5432" elasticsearch: image: elasticsearch:5.0.0 ports: - "9200:9200" @bsideup

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

IDE integration? @bsideup

Slide 24

Slide 24 text

Fighting with Docker environment

Slide 25

Slide 25 text

There is no place like

Slide 26

Slide 26 text

There is no place like … unless there is

Slide 27

Slide 27 text

Can we improve that?

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

Testcontainers • http://github.com/testcontainers/testcontainers-java • Wraps docker-java library • Docker environment discovery (Win, Mac, Linux) • Containers cleanup on JVM shutdown @bsideup

Slide 30

Slide 30 text

As simple as PostgreSQLContainer postgresql = new PostgreSQLContainer() GenericContainer redis = new GenericContainer("redis:3") .withExposedPorts(6379) @bsideup

Slide 31

Slide 31 text

Users

Slide 32

Slide 32 text

@bsideup

Slide 33

Slide 33 text

Demo

Slide 34

Slide 34 text

1.6.x Jan, 2018 @bsideup

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

1.6.x • Kafka module • “Ryuk” Jan, 2018 @bsideup

Slide 37

Slide 37 text

1.7.x Apr, 2018 @bsideup

Slide 38

Slide 38 text

1.7.x • Maven BOM Apr, 2018 @bsideup org.testcontainers testcontainers-bom 1.11.2 bom import

Slide 39

Slide 39 text

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) );

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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)); // ... }

Slide 42

Slide 42 text

1.8.x Jun, 2018 @bsideup

Slide 43

Slide 43 text

1.8.x • OkHttp transport Jun, 2018 @bsideup

Slide 44

Slide 44 text

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 ) {} }

Slide 45

Slide 45 text

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" } }

Slide 46

Slide 46 text

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 )

Slide 47

Slide 47 text

1.8.x • OkHttp transport • Test framework
 agnostic • Docker cred. helpers • copyFileToContainer • Pulsar module • Couchbase module • Cassandra module Jun, 2018 @bsideup

Slide 48

Slide 48 text

1.9.x Sep, 2018 @bsideup

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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);

Slide 53

Slide 53 text

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!")); }

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

1.10.x Nov, 2018 @bsideup

Slide 58

Slide 58 text

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()); } }

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

1.11.x Mar, 2019 @bsideup

Slide 66

Slide 66 text

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"); }

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

1.12.x Jul, 2019 @bsideup

Slide 72

Slide 72 text

1.12.x • dependsOn Jul, 2019 @bsideup @Rule KafkaContainer kafka = new KafkaContainer(); @Rule SchemaContainer schemaRegistryContainer = new SchemaContainer() .withKafka(kafka) .dependsOn(kafka);

Slide 73

Slide 73 text

1.12.x • dependsOn • Improved pull handling Jul, 2019 @bsideup 11:15:50.598 INFO [ibmcom/db2express-c:latest] - Pulling docker image: ibmcom/db2express-c:latest. Please 19:34:25.198 INFO [ibmcom/db2express-c:latest] - Pulling image 19:34:25.198 INFO [ibmcom/db2express-c:latest] - Pulling image layers: 0 pending, 0 downloaded, 0 extr 19:34:25.967 INFO [ibmcom/db2express-c:latest] - Pulling image layers: 12 pending, 1 downloaded, 0 extr 19:34:27.363 INFO [ibmcom/db2express-c:latest] - Pulling image layers: 11 pending, 2 downloaded, 0 extr 19:34:58.519 ERROR [ibmcom/db2express-c:latest] - Docker image pull has not made progress in 30s - abortin 19:34:58.564 ERROR [ibmcom/db2express-c:latest] - Failed to pull image: ibmcom/db2express-c:latest. Please Before: After:

Slide 74

Slide 74 text

1.12.x • dependsOn • Improved pull handling • Azure Pipelines for Windows testing Jul, 2019 @bsideup

Slide 75

Slide 75 text

1.12.x • dependsOn • Improved pull handling • Azure Pipelines for Windows testing Jul, 2019 @bsideup

Slide 76

Slide 76 text

1.12.x • dependsOn • Improved pull handling • Azure Pipelines for Windows testing • DB2 and RabbitMQ modules Jul, 2019 @bsideup

Slide 77

Slide 77 text

Roadmap

Slide 78

Slide 78 text

new DSL

Slide 79

Slide 79 text

What’s wrong with the current one?

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

public class KafkaContainer extends GenericContainer {} public class GenericContainer> /* */ {} @bsideup

Slide 87

Slide 87 text

public class KafkaContainer extends GenericContainer {} public class GenericContainer> /* */ {} https://youtrack.jetbrains.com/issue/KT-17186 https://stackoverflow.com/questions/39163749/how-to-use-a-java-self-bounded-class-in-scala @bsideup

Slide 88

Slide 88 text

❌ Hard to maintain @bsideup

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

❌ 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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

✓ Super easy to maintain @bsideup

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

✓ 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

Slide 103

Slide 103 text

GraalVM focus

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

@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

Slide 106

Slide 106 text

@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)));

Slide 107

Slide 107 text

“container core”

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

Testcontainers 1.x architecture Core
 (Docker, GenericContainer, 
 JUnit 4 int, test lifecycle) JUnit Jupiter int. Modules like MySQL/Kafka/… @bsideup

Slide 111

Slide 111 text

Testcontainers 2.x architecture Container-core
 (GenericContainer) @bsideup

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

No content

Slide 116

Slide 116 text

Usability Flexibility Speed Features @bsideup

Slide 117

Slide 117 text

Usability Flexibility Speed Features @bsideup

Slide 118

Slide 118 text

Usability Flexibility Speed Features @bsideup

Slide 119

Slide 119 text

Please welcome…

Slide 120

Slide 120 text

Reusable containers

Slide 121

Slide 121 text

Reusable containers

Slide 122

Slide 122 text

Demo

Slide 123

Slide 123 text

@bsideup

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

✓ 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

Slide 128

Slide 128 text

✓ 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) ✓ Alpha version is available NOW @bsideup

Slide 129

Slide 129 text

Takeaways • https://testcontainers.org • Works on Linux, Mac and Windows • …including CIs like Jenkins, Travis, CircleCI, GH Actions, Azure Pipelines, … • Provides a great balance between 
 flexibility, usability, speed and features
 @bsideup

Slide 130

Slide 130 text

@bsideup bsideup