Unit testing our code on the JVM is well catered for with a lot of great tools that are mature and reliable – things tend to just work. Integrated testing, however, is another matter. Any time we face a situation where we need to involve non-JVM elements in our tests, we’re faced with painful environment setup and repeatability issues. Testcontainers aims to make integrated tests a little less unpleasant, through the power of Docker. Databases, Web browsers – in fact anything available as a Docker image – can be made available as a component to use in our tests.
In this talk, we’ll go through the motivations for building Testcontainers, its features, as well as some examples of using it in practice for testing various types of components.
Neo4j - How KGs are shaping the future of Generative AI at AWS Summit London ...
Testcontainers - Geekout EE 2017 presentation
1.
2. Who am I?
Richard North
• Lifelong geek
• Java/iOS/web/'devops' - 'full stack'
• Ex-consultant, now at Skyscanner
• UK and Japan
• Proud father of two!
@whichrich
32. "we can build an environment in seconds"
- quickly, cheaply
- always the same
- definition is version controlled
- Docker Hub - plentiful base images
35. Testcontainers
• Manage Dockerized external dependencies via a Java object facade
• JUnit integration - Starts/stops containers for each class/method
• Reliability:
• start from clean state
• isolated instances
• port randomisation
• tag-based versioning
• Java JUnit support; also Spock, Scala and Python wrappers/forks
36. Testcontainers project
• Initial versions mid 2015
• 36 contributors over time; Sergei Egorov (@bsideup) is the main co-maintainer
• Some users:
• ZeroTurnaround
• Spring Data
• Apache
• Zalando
• Alfalab
• Zipkin
• Others...!
40. public interface Cache {
void put(String key, String value);
String get(String key);
}
public class RedisBackedCache implements Cache {
// Uses Redis...
}
41. public class RedisBackedCacheTest {
private Cache cache;
@Before
public void setup() {
cache = new RedisBackedCache("localhost", 6379);
}
@Test
public void testGetAndSetAValue() {
cache.put("foo", "bar");
final String retrievedValue = cache.get("foo");
assertEquals("The retrieved value is the same as the inserted value", "bar", retrievedValue);
}
}
42. public class RedisBackedCacheTest {
@Rule
public static GenericContainer redis = new GenericContainer("redis:3.2.8");
private Cache cache;
@Before
public void setup() {
cache = new RedisBackedCache(???, ???);
}
@Test
public void testGetAndSetAValue() {
cache.put("foo", "bar");
final String retrievedValue = cache.get("foo");
assertEquals("The retrieved value is the same as the inserted value", "bar", retrievedValue);
}
}
43. public class RedisBackedCacheTest {
@Rule
public static GenericContainer redis = new GenericContainer("redis:3.2.8")
.withExposedPorts(6379);
private Cache cache;
@Before
public void setup() {
cache = new RedisBackedCache(redis.getContainerIpAddress(), redis.getMappedPort(6379));
}
@Test
public void testGetAndSetAValue() {
cache.put("foo", "bar");
final String retrievedValue = cache.get("foo");
assertEquals("The retrieved value is the same as the inserted value", "bar", retrievedValue);
}
}
45. • Automatic discovery of local docker environment
• Pull images or build from Dockerfile
• Start/stop container
• Wait for it to be ready (log string / listening port / protocol-
specific)
• Port mapping
• Clean up
46. What have we avoided?
• No need to install the dependency
• No need to keep it running, or make sure it's running
• No concern over version or configuration differences
• No differences between what runs on CI and locally
• No port clashes, no shared state unless we want it
47. What have we gained?
• Repeatability - locked version redis:3.2.8
• Debuggable locally - runnable in IDE
• Parallelizable
• Runs anywhere that Docker runs
48. 'Anywhere Docker runs'
Automatic discovery:
• Docker for Mac and Docker for Windows
• Docker Machine
• Uses a running machine instance, or default
• Automatically starts up Docker Machine if needed
• Docker on Linux
• Cloud CI
• Travis CI
• Circle CI
• Docker in Docker
• ... or wherever DOCKER_HOST is set
50. A simple DAO API
public interface UsefulDao {
void putAThing(String name, SomeObject value);
SomeObject getAThingByJsonId(Integer id);
}
51. A corresponding test
public class UsefulDaoTest {
private UsefulDao dao;
@Before
public void setUp() throws Exception {
// Instantiate the DAO
// Connect
// Create schema and data
}
@Test
public void testPutAndGetByJsonIndex() throws Exception {
// INSERT and SELECT something
}
}
52. How can we test this with no
database?
Embedded database!
54. JSONB is a PostgreSQL data type - how can we test this?
• Embedded database?
• Run PostgreSQL through our build script?
• Hope that the developer/CI environment has the right version of
PostgreSQL?
• Give up, and don't use database features we can't easily test? !
• Don't test interactions with the DB, and hope that it works in prod? !!
• Can we use Testcontainers..?
55. Yes we can!
@Rule
public PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:9.6.2");
Access at test-time
postgres.getJdbcUrl(); // Unique URL for a container instance
postgres.getUsername();
postgres.getPassword();
57. public class SeleniumTest {
private WebDriver driver;
@Before
public void setUp() throws Exception {
// Connect to remote selenium grid
// or start a local browser process (Headless? Real browser?)
}
@Test
public void testSimple() throws IOException {
...
}
}
58. public class SeleniumTest {
@Rule
public BrowserWebDriverContainer chrome =
new BrowserWebDriverContainer()
.withDesiredCapabilities(DesiredCapabilities.chrome());
private WebDriver driver;
@Before
public void setUp() throws Exception {
driver = chrome.getWebDriver();
}
@Test
public void testSimple() throws IOException {
...
}
}
61. Debug via VNC!
Set a breakpoint:
Get a VNC URL
chrome.getVncAddress() // e.g. "vnc://vnc:secret@localhost:32786"
Connect
$ open vnc://vnc:secret@localhost:32786
62. Recap so far
• Using an image from Docker Hub as a dependency
• Specialised database support
• Selenium testing
64. Build a container image at test time
Allows:
• Running code in real, prod-like Docker image
• Create an image that's not available from a registry
• Parameterized builds - using a DSL
Doesn't require a separate phase for build/test pipeline
65. Build a container image at test time -
Dockerfile
FROM tomcat:8.5.15
COPY service.war /usr/local/tomcat/webapps/my-service.war
66. Build a container image at test time -
Dockerfile
@Rule
public GenericContainer server = new GenericContainer(
new ImageFromDockerfile()
.withFileFromFile("Dockerfile", new File("./Dockerfile"))
.withFileFromFile("service.war", new File("target/my-service.war")))
.withExposedPorts(8080);
@Test
public void simpleTest() {
// do something with the server - port is server.getMappedPort(8080));
}
67. Build a container image at test time - DSL
@Rule
public GenericContainer server = new GenericContainer(
new ImageFromDockerfile()
.withFileFromFile("service.war", new File("target/my-service.war"))
.withDockerfileFromBuilder(builder -> {
builder
.from("tomcat:8.5.15")
.copy("service.war", "/usr/local/tomcat/webapps/my-service.war")
.build();
}))
.withExposedPorts(8080);
@Test
public void simpleTest() {
// do something with the server - port is server.getMappedPort(8080));
}
70. Multiple containers as JUnit rules
One way?
public class SimpleSystemTest {
@ClassRule
public GenericContainer db = new GenericContainer("mongo:3.0.15");
@ClassRule
public GenericContainer cache = new GenericContainer("redis:3.2.8");
@ClassRule
public GenericContainer search = new GenericContainer("elasticsearch:5.4.0");
// ... tests ...
}
71. Using Docker Compose during a test
@Rule
public DockerComposeContainer backend = new DockerComposeContainer(new File("./docker-compose.backend.yml"))
.withExposedService("db", 27017)
.withExposedService("cache", 6379)
.withExposedService("search", 9200);
@Test
public void simpleTest() {
// obtain host/ports for each container as follows:
// backend.getServiceHost("db", 27017);
// backend.getServicePort("db", 27017);
// ...
}
72. Docker Compose in Testcontainers
• Unique, random, name prefix and isolated network - allows concurrent usage
One usage mode:
• Use docker-compose up -f ... during local dev (overrides file to expose
ports)
• Run tests concurrently via Testcontainers without stopping local
environment
• Seamless transition to CI - use Testcontainers
73. Summary
• Generic image container
• Specialised Database container
• Selenium containers with video recording and VNC debugging
• Building a Dockerfile
• Docker Compose
76. 'Version 2'
• API tidyup
• decouple from JUnit 4 and support other frameworks directly
77. Core elements as a library
• high-level Docker object API as a library, for more than just
testing usage
• planning collaboration with Arquillian Cube project team !
79. Conclusion
• Hopefully another useful tool for your testing toolbox
• Easy to use, powerful features for many scenarios
• Please try it out yourselves!