Introduction to
Testcontainers
Oleg Šelajev
Developer relations
@shelajev
github.com/shelajev
oleg@atomicjar.com
Cora Iberkleid
Developer relations
@ciberkleid
github.com/ciberkleid
ciberkleid@vmware.com
Unit tests
Integration
tests
e2e
Common approaches to integration testing
In Integration tests we are interested in verifying the behavior and interactions of
multiple components.
For this purpose we can use:
● Shared instances
● Local installation
● In memory solutions
○ Mock server (Wiremock, Loki…)
○ In-memory service (h2, hsql...)
● Docker Compose
• Container lifecycle &
cleanup
• Container & service
configuration
• Integration with
frameworks or tests
• Container lifecycle &
cleanup
• Container & service
configuration
• Integration with
frameworks or tests
Testcontainers-java
• Created 7 years ago (Docker is 8 years old)
• github.com/testcontainers/testcontainers-java
• Uses docker-java API
• Integrates with frameworks, like Spring, JUnit
• Works with anything that runs in a Docker container
Growing ecosystem of modules
https:/
/www.thoughtworks.com/en-us/radar/languages-and-frameworks/testcontainers
We think it's a useful default
option for creating
a reliable environment for
running tests.
…
Our teams have consistently
found this library of
programmable, lightweight
and disposable containers to
make functional tests more
reliable.
start.spring.io has it :)
Demo 0: Lifecycle!
● JUnit 5 integration
● Container lifecycle
● Shared containers (between test methods)
@Testcontainers
@Slf4j
public class LifeCycleTest {
@Container
GenericContainer container = new
GenericContainer(DockerImageName.parse("myRepo/myImg"));
public LifeCycleTest() {
log.info("In Constructor. Class instance: ", this);
}
@BeforeAll
public static void beforeAllMethod() {
log.info("In @BeforeAll. Static method.");
}
@BeforeEach
public void beforeEachMethod() {
log.info("In @BeforeEach. Class instance: " + this
+ ", Container id: ", container.getContainerId());
}
@Test
public void test1() {
log.info("In @Test 1. Class instance: ", this);
}
@Test
public void test2() {
log.info("In @Test 2. Class instance: ", this);
}
@AfterEach
public void afterEachMethod() {
log.info("In @AfterEach. Class instance: ", this);
}
@AfterAll
public static void afterAllMethod() {
log.info("In @AfterAll. Static method.");
}
}
@Testcontainers
@Slf4j
public class LifeCycleTest {
@Container
GenericContainer container = new
GenericContainer(DockerImageName.parse("myRepo/myImg"));
public LifeCycleTest() {
log.info("In Constructor. Class instance: ", this);
}
@BeforeAll
public static void beforeAllMethod() {
log.info("In @BeforeAll. Static method.");
}
@BeforeEach
public void beforeEachMethod() {
log.info("In @BeforeEach. Class instance: " + this
+ ", Container id: ", container.getContainerId());
}
@Test
public void test1() {
log.info("In @Test 1. Class instance: ", this);
}
@Test
public void test2() {
log.info("In @Test 2. Class instance: ", this);
}
@AfterEach
public void afterEachMethod() {
log.info("In @AfterEach. Class instance: ", this);
}
@AfterAll
public static void afterAllMethod() {
log.info("In @AfterAll. Static method.");
}
}
In @BeforeAll. Static method.
In Constructor. Class instance: LifeCycleTest@17d88132
Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
Connected to docker
Pulling docker image: testcontainers/ryuk
Ryuk started - will monitor and terminate Testcontainers containers on JVM
exit
Checking the system...
✔ Docker server version should be at least 1.6.0
✔ Docker environment should have more than 2GB free disk space
Pulling docker image: myRepo/myImg
Container myRepo/myImg is starting:
b1c63a2738a6131ac06dd90bc334e621099b8a9d0092cfbc8899954495bd57c5
In @BeforeEach. Class instance: LifeCycleTest@17d88132, Container id:
b1c63a2738a6131ac06dd90bc334e621099b8a9d0092cfbc8899954495bd57c5
@BeforeEach, Test1:
Connect to docker
Start ryuk
Check system
Start container
Execute BeforeEach method
Test 1
@Testcontainers
@Slf4j
public class LifeCycleTest {
@Container
GenericContainer container = new
GenericContainer(DockerImageName.parse("myRepo/myImg"));
public LifeCycleTest() {
log.info("In Constructor. Class instance: ", this);
}
@BeforeAll
public static void beforeAllMethod() {
log.info("In @BeforeAll. Static method.");
}
@BeforeEach
public void beforeEachMethod() {
log.info("In @BeforeEach. Class instance: " + this
+ ", Container id: ", container.getContainerId());
}
@Test
public void test1() {
log.info("In @Test 1. Class instance: ", this);
}
@Test
public void test2() {
log.info("In @Test 2. Class instance: ", this);
}
@AfterEach
public void afterEachMethod() {
log.info("In @AfterEach. Class instance: ", this);
}
@AfterAll
public static void afterAllMethod() {
log.info("In @AfterAll. Static method.");
}
}
In @BeforeAll. Static method.
In Constructor. Class instance: LifeCycleTest@17d88132
Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
Connected to docker
Pulling docker image: testcontainers/ryuk
Ryuk started - will monitor and terminate Testcontainers containers on JVM
exit
Checking the system...
✔ Docker server version should be at least 1.6.0
✔ Docker environment should have more than 2GB free disk space
Pulling docker image: myRepo/myImg
Container myRepo/myImg is starting:
b1c63a2738a6131ac06dd90bc334e621099b8a9d0092cfbc8899954495bd57c5
In @BeforeEach. Class instance: LifeCycleTest@17d88132, Container id:
b1c63a2738a6131ac06dd90bc334e621099b8a9d0092cfbc8899954495bd57c5
In @Test 1. Class instance: LifeCycleTest@17d88132
In @AfterEach. Class instance: LifeCycleTest@17d88132
Ryuk removed container and associated volume(s): myRepo/myImg
After Test 1 “AfterEach”,
Ryuk cleans up container
Test 1
@Testcontainers
@Slf4j
public class LifeCycleTest {
@Container
GenericContainer container = new
GenericContainer(DockerImageName.parse("myRepo/myImg"));
public LifeCycleTest() {
log.info("In Constructor. Class instance: ", this);
}
@BeforeAll
public static void beforeAllMethod() {
log.info("In @BeforeAll. Static method.");
}
@BeforeEach
public void beforeEachMethod() {
log.info("In @BeforeEach. Class instance: " + this
+ ", Container id: ", container.getContainerId());
}
@Test
public void test1() {
log.info("In @Test 1. Class instance: ", this);
}
@Test
public void test2() {
log.info("In @Test 2. Class instance: ", this);
}
@AfterEach
public void afterEachMethod() {
log.info("In @AfterEach. Class instance: ", this);
}
@AfterAll
public static void afterAllMethod() {
log.info("In @AfterAll. Static method.");
}
}
In @BeforeAll. Static method.
In Constructor. Class instance: LifeCycleTest@17d88132
Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
Connected to docker
Pulling docker image: testcontainers/ryuk
Ryuk started - will monitor and terminate Testcontainers containers on JVM
exit
Checking the system...
✔ Docker server version should be at least 1.6.0
✔ Docker environment should have more than 2GB free disk space
Pulling docker image: myRepo/myImg
Container myRepo/myImg is starting:
b1c63a2738a6131ac06dd90bc334e621099b8a9d0092cfbc8899954495bd57c5
In @BeforeEach. Class instance: LifeCycleTest@17d88132, Container id:
b1c63a2738a6131ac06dd90bc334e621099b8a9d0092cfbc8899954495bd57c5
In @Test 1. Class instance: LifeCycleTest@17d88132
In @AfterEach. Class instance: LifeCycleTest@17d88132
Ryuk removed container and associated volume(s): myRepo/myImg
In Constructor. Class instance: LifeCycleTest@42d236fb
Container myRepo/myImg is starting:
20e4227f6d5366c7f6ccb2ebf09eb5666bb75d20a4c307c6026870dc0fc0d3a1
In @BeforeEach. Class instance: LifeCycleTest@42d236fb, Container id:
20e4227f6d5366c7f6ccb2ebf09eb5666bb75d20a4c307c6026870dc0fc0d3a1
@BeforeEach, Test2:
Start new container
Execute BeforeEach method
Test 2
Test 1
@Testcontainers
@Slf4j
public class LifeCycleTest {
@Container
static GenericContainer container = new
GenericContainer(DockerImageName.parse("myRepo/myImg"));
public LifeCycleTest() {
log.info("In Constructor. Class instance: ", this);
}
@BeforeAll
public static void beforeAllMethod() {
log.info("In @BeforeAll. Static method.");
}
@BeforeEach
public void beforeEachMethod() {
log.info("In @BeforeEach. Class instance: " + this
+ ", Container id: ", container.getContainerId());
}
@Test
public void test1() {
log.info("In @Test 1. Class instance: ", this);
}
@Test
public void test2() {
log.info("In @Test 2. Class instance: ", this);
}
@AfterEach
public void afterEachMethod() {
log.info("In @AfterEach. Class instance: ", this);
}
@AfterAll
public static void afterAllMethod() {
log.info("In @AfterAll. Static method.");
}
}
@Testcontainers
@Slf4j
public class LifeCycleTest {
@Container
static GenericContainer container = new
GenericContainer(DockerImageName.parse("myRepo/myImg"));
public LifeCycleTest() {
log.info("In Constructor. Class instance: ", this);
}
@BeforeAll
public static void beforeAllMethod() {
log.info("In @BeforeAll. Static method.");
}
@BeforeEach
public void beforeEachMethod() {
log.info("In @BeforeEach. Class instance: " + this
+ ", Container id: ", container.getContainerId());
}
@Test
public void test1() {
log.info("In @Test 1. Class instance: ", this);
}
@Test
public void test2() {
log.info("In @Test 2. Class instance: ", this);
}
@AfterEach
public void afterEachMethod() {
log.info("In @AfterEach. Class instance: ", this);
}
@AfterAll
public static void afterAllMethod() {
log.info("In @AfterAll. Static method.");
}
}
Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
Connected to docker
Ryuk started - will monitor and terminate Testcontainers containers on JVM
exit
Checking the system...
✔ Docker server version should be at least 1.6.0
✔ Docker environment should have more than 2GB free disk space
Container myRepo/myImg is starting:
8d8ff20a9f25dd8b0d4cd025406bdebfa9dcf5ab637ef53b5a815d97bc739b5a
In @BeforeAll. Static method.
In Constructor. Class instance: LifeCycleTest@63a5d002
In @BeforeEach. Class instance: LifeCycleTest@63a5d002, Container id:
8d8ff20a9f25dd8b0d4cd025406bdebfa9dcf5ab637ef53b5a815d97bc739b5a
In @Test 1. Class instance: LifeCycleTest@63a5d002
In @AfterEach. Class instance: LifeCycleTest@63a5d002
In Constructor. Class instance: LifeCycleTest@60e949e1
In @BeforeEach. Class instance: LifeCycleTest@60e949e1, Container id:
8d8ff20a9f25dd8b0d4cd025406bdebfa9dcf5ab637ef53b5a815d97bc739b5a
In @Test 2. Class instance: LifeCycleTest@60e949e1
In @AfterEach. Class instance: LifeCycleTest@60e949e1
In @AfterAll. Static method.
Ryuk removed container and associated volume(s): myRepo/myImg
Container is reused
across tests
Test 2
Test 1
Demo 1: basics :)
● Docker container configuration
○ ports
○ logs
○ environment variables
○ startup commands
● Dockerfile support
https://github.com/shelajev/spring-one-tour-2022
Demo 2: common use cases
● Testcontainers modules
● Spring dynamic configuration
● Manual lifecycle control
● Containerized service initialization
● Database initialization strategies
● Chaos testing
https://github.com/shelajev/spring-one-tour-2022
TC - cloud
testcontainers.cloud
bit.ly/tcc-springone-tour22
testcontainers.cloud
What’s next?
— github.com/testcontainers/testcontainers-java
— testcontainers.org
— slack.testcontainers.org

Introduction to Testcontainers

  • 1.
  • 2.
    Oleg Šelajev Developer relations @shelajev github.com/shelajev oleg@atomicjar.com CoraIberkleid Developer relations @ciberkleid github.com/ciberkleid ciberkleid@vmware.com
  • 4.
  • 5.
    Common approaches tointegration testing In Integration tests we are interested in verifying the behavior and interactions of multiple components. For this purpose we can use: ● Shared instances ● Local installation ● In memory solutions ○ Mock server (Wiremock, Loki…) ○ In-memory service (h2, hsql...) ● Docker Compose
  • 6.
    • Container lifecycle& cleanup • Container & service configuration • Integration with frameworks or tests
  • 7.
    • Container lifecycle& cleanup • Container & service configuration • Integration with frameworks or tests
  • 8.
    Testcontainers-java • Created 7years ago (Docker is 8 years old) • github.com/testcontainers/testcontainers-java • Uses docker-java API • Integrates with frameworks, like Spring, JUnit • Works with anything that runs in a Docker container
  • 10.
  • 11.
    https:/ /www.thoughtworks.com/en-us/radar/languages-and-frameworks/testcontainers We think it'sa useful default option for creating a reliable environment for running tests. … Our teams have consistently found this library of programmable, lightweight and disposable containers to make functional tests more reliable.
  • 12.
  • 13.
    Demo 0: Lifecycle! ●JUnit 5 integration ● Container lifecycle ● Shared containers (between test methods)
  • 14.
    @Testcontainers @Slf4j public class LifeCycleTest{ @Container GenericContainer container = new GenericContainer(DockerImageName.parse("myRepo/myImg")); public LifeCycleTest() { log.info("In Constructor. Class instance: ", this); } @BeforeAll public static void beforeAllMethod() { log.info("In @BeforeAll. Static method."); } @BeforeEach public void beforeEachMethod() { log.info("In @BeforeEach. Class instance: " + this + ", Container id: ", container.getContainerId()); } @Test public void test1() { log.info("In @Test 1. Class instance: ", this); } @Test public void test2() { log.info("In @Test 2. Class instance: ", this); } @AfterEach public void afterEachMethod() { log.info("In @AfterEach. Class instance: ", this); } @AfterAll public static void afterAllMethod() { log.info("In @AfterAll. Static method."); } }
  • 15.
    @Testcontainers @Slf4j public class LifeCycleTest{ @Container GenericContainer container = new GenericContainer(DockerImageName.parse("myRepo/myImg")); public LifeCycleTest() { log.info("In Constructor. Class instance: ", this); } @BeforeAll public static void beforeAllMethod() { log.info("In @BeforeAll. Static method."); } @BeforeEach public void beforeEachMethod() { log.info("In @BeforeEach. Class instance: " + this + ", Container id: ", container.getContainerId()); } @Test public void test1() { log.info("In @Test 1. Class instance: ", this); } @Test public void test2() { log.info("In @Test 2. Class instance: ", this); } @AfterEach public void afterEachMethod() { log.info("In @AfterEach. Class instance: ", this); } @AfterAll public static void afterAllMethod() { log.info("In @AfterAll. Static method."); } } In @BeforeAll. Static method. In Constructor. Class instance: LifeCycleTest@17d88132 Found Docker environment with local Unix socket (unix:///var/run/docker.sock) Connected to docker Pulling docker image: testcontainers/ryuk Ryuk started - will monitor and terminate Testcontainers containers on JVM exit Checking the system... ✔ Docker server version should be at least 1.6.0 ✔ Docker environment should have more than 2GB free disk space Pulling docker image: myRepo/myImg Container myRepo/myImg is starting: b1c63a2738a6131ac06dd90bc334e621099b8a9d0092cfbc8899954495bd57c5 In @BeforeEach. Class instance: LifeCycleTest@17d88132, Container id: b1c63a2738a6131ac06dd90bc334e621099b8a9d0092cfbc8899954495bd57c5 @BeforeEach, Test1: Connect to docker Start ryuk Check system Start container Execute BeforeEach method Test 1
  • 16.
    @Testcontainers @Slf4j public class LifeCycleTest{ @Container GenericContainer container = new GenericContainer(DockerImageName.parse("myRepo/myImg")); public LifeCycleTest() { log.info("In Constructor. Class instance: ", this); } @BeforeAll public static void beforeAllMethod() { log.info("In @BeforeAll. Static method."); } @BeforeEach public void beforeEachMethod() { log.info("In @BeforeEach. Class instance: " + this + ", Container id: ", container.getContainerId()); } @Test public void test1() { log.info("In @Test 1. Class instance: ", this); } @Test public void test2() { log.info("In @Test 2. Class instance: ", this); } @AfterEach public void afterEachMethod() { log.info("In @AfterEach. Class instance: ", this); } @AfterAll public static void afterAllMethod() { log.info("In @AfterAll. Static method."); } } In @BeforeAll. Static method. In Constructor. Class instance: LifeCycleTest@17d88132 Found Docker environment with local Unix socket (unix:///var/run/docker.sock) Connected to docker Pulling docker image: testcontainers/ryuk Ryuk started - will monitor and terminate Testcontainers containers on JVM exit Checking the system... ✔ Docker server version should be at least 1.6.0 ✔ Docker environment should have more than 2GB free disk space Pulling docker image: myRepo/myImg Container myRepo/myImg is starting: b1c63a2738a6131ac06dd90bc334e621099b8a9d0092cfbc8899954495bd57c5 In @BeforeEach. Class instance: LifeCycleTest@17d88132, Container id: b1c63a2738a6131ac06dd90bc334e621099b8a9d0092cfbc8899954495bd57c5 In @Test 1. Class instance: LifeCycleTest@17d88132 In @AfterEach. Class instance: LifeCycleTest@17d88132 Ryuk removed container and associated volume(s): myRepo/myImg After Test 1 “AfterEach”, Ryuk cleans up container Test 1
  • 17.
    @Testcontainers @Slf4j public class LifeCycleTest{ @Container GenericContainer container = new GenericContainer(DockerImageName.parse("myRepo/myImg")); public LifeCycleTest() { log.info("In Constructor. Class instance: ", this); } @BeforeAll public static void beforeAllMethod() { log.info("In @BeforeAll. Static method."); } @BeforeEach public void beforeEachMethod() { log.info("In @BeforeEach. Class instance: " + this + ", Container id: ", container.getContainerId()); } @Test public void test1() { log.info("In @Test 1. Class instance: ", this); } @Test public void test2() { log.info("In @Test 2. Class instance: ", this); } @AfterEach public void afterEachMethod() { log.info("In @AfterEach. Class instance: ", this); } @AfterAll public static void afterAllMethod() { log.info("In @AfterAll. Static method."); } } In @BeforeAll. Static method. In Constructor. Class instance: LifeCycleTest@17d88132 Found Docker environment with local Unix socket (unix:///var/run/docker.sock) Connected to docker Pulling docker image: testcontainers/ryuk Ryuk started - will monitor and terminate Testcontainers containers on JVM exit Checking the system... ✔ Docker server version should be at least 1.6.0 ✔ Docker environment should have more than 2GB free disk space Pulling docker image: myRepo/myImg Container myRepo/myImg is starting: b1c63a2738a6131ac06dd90bc334e621099b8a9d0092cfbc8899954495bd57c5 In @BeforeEach. Class instance: LifeCycleTest@17d88132, Container id: b1c63a2738a6131ac06dd90bc334e621099b8a9d0092cfbc8899954495bd57c5 In @Test 1. Class instance: LifeCycleTest@17d88132 In @AfterEach. Class instance: LifeCycleTest@17d88132 Ryuk removed container and associated volume(s): myRepo/myImg In Constructor. Class instance: LifeCycleTest@42d236fb Container myRepo/myImg is starting: 20e4227f6d5366c7f6ccb2ebf09eb5666bb75d20a4c307c6026870dc0fc0d3a1 In @BeforeEach. Class instance: LifeCycleTest@42d236fb, Container id: 20e4227f6d5366c7f6ccb2ebf09eb5666bb75d20a4c307c6026870dc0fc0d3a1 @BeforeEach, Test2: Start new container Execute BeforeEach method Test 2 Test 1
  • 18.
    @Testcontainers @Slf4j public class LifeCycleTest{ @Container static GenericContainer container = new GenericContainer(DockerImageName.parse("myRepo/myImg")); public LifeCycleTest() { log.info("In Constructor. Class instance: ", this); } @BeforeAll public static void beforeAllMethod() { log.info("In @BeforeAll. Static method."); } @BeforeEach public void beforeEachMethod() { log.info("In @BeforeEach. Class instance: " + this + ", Container id: ", container.getContainerId()); } @Test public void test1() { log.info("In @Test 1. Class instance: ", this); } @Test public void test2() { log.info("In @Test 2. Class instance: ", this); } @AfterEach public void afterEachMethod() { log.info("In @AfterEach. Class instance: ", this); } @AfterAll public static void afterAllMethod() { log.info("In @AfterAll. Static method."); } }
  • 19.
    @Testcontainers @Slf4j public class LifeCycleTest{ @Container static GenericContainer container = new GenericContainer(DockerImageName.parse("myRepo/myImg")); public LifeCycleTest() { log.info("In Constructor. Class instance: ", this); } @BeforeAll public static void beforeAllMethod() { log.info("In @BeforeAll. Static method."); } @BeforeEach public void beforeEachMethod() { log.info("In @BeforeEach. Class instance: " + this + ", Container id: ", container.getContainerId()); } @Test public void test1() { log.info("In @Test 1. Class instance: ", this); } @Test public void test2() { log.info("In @Test 2. Class instance: ", this); } @AfterEach public void afterEachMethod() { log.info("In @AfterEach. Class instance: ", this); } @AfterAll public static void afterAllMethod() { log.info("In @AfterAll. Static method."); } } Found Docker environment with local Unix socket (unix:///var/run/docker.sock) Connected to docker Ryuk started - will monitor and terminate Testcontainers containers on JVM exit Checking the system... ✔ Docker server version should be at least 1.6.0 ✔ Docker environment should have more than 2GB free disk space Container myRepo/myImg is starting: 8d8ff20a9f25dd8b0d4cd025406bdebfa9dcf5ab637ef53b5a815d97bc739b5a In @BeforeAll. Static method. In Constructor. Class instance: LifeCycleTest@63a5d002 In @BeforeEach. Class instance: LifeCycleTest@63a5d002, Container id: 8d8ff20a9f25dd8b0d4cd025406bdebfa9dcf5ab637ef53b5a815d97bc739b5a In @Test 1. Class instance: LifeCycleTest@63a5d002 In @AfterEach. Class instance: LifeCycleTest@63a5d002 In Constructor. Class instance: LifeCycleTest@60e949e1 In @BeforeEach. Class instance: LifeCycleTest@60e949e1, Container id: 8d8ff20a9f25dd8b0d4cd025406bdebfa9dcf5ab637ef53b5a815d97bc739b5a In @Test 2. Class instance: LifeCycleTest@60e949e1 In @AfterEach. Class instance: LifeCycleTest@60e949e1 In @AfterAll. Static method. Ryuk removed container and associated volume(s): myRepo/myImg Container is reused across tests Test 2 Test 1
  • 20.
    Demo 1: basics:) ● Docker container configuration ○ ports ○ logs ○ environment variables ○ startup commands ● Dockerfile support https://github.com/shelajev/spring-one-tour-2022
  • 21.
    Demo 2: commonuse cases ● Testcontainers modules ● Spring dynamic configuration ● Manual lifecycle control ● Containerized service initialization ● Database initialization strategies ● Chaos testing https://github.com/shelajev/spring-one-tour-2022
  • 22.
  • 23.
  • 24.
    What’s next? — github.com/testcontainers/testcontainers-java —testcontainers.org — slack.testcontainers.org