TestContainers
Integration testing without the hassle
@antonarhipov
Why integration testing?
1. # of tests per time unit
1. # of tests per time unit
2. Complex setup
1. Reproducible environment
1. Reproducible environment
2. Both for development and CI
1. Reproducible environment
2. Both for development and CI
3. Isolated
1. Reproducible environment
2. Both for development and CI
3. Isolated
4. As real as possible
1. Reproducible environment
2. Both for development and CI
3. Isolated
4. As real as possible
5. Cross-platform
1. Reproducible environment
2. Both for development and CI
3. Isolated
4. As real as possible
5. Cross-platform
6. Easy to set up, use & maintain
Docker Compose FTW!
redis:

image: redis

ports:

- "6379:6379"

postgres:

image: postgres

ports:

- "5432:5432"

elasticsearch:

image: elasticsearch:5.0.0

ports:

- "9200:9200"

TestContainers to the rescue!
How does it work?
https://github.com/docker-java/docker-java
Docker environment discovery
Will start docker machine if it’s not started yet
Containers cleanup on JVM shutdown
GenericContainer redis =
new GenericContainer("redis:3.0.6")
.withExposedPorts(6379);
GenericContainer redis =
new GenericContainer("redis:3.0.6")
.withExposedPorts(6379);
PostgreSQLContainer postgreSQLContainer =
new PostgreSQLContainer("postgres:9.6.2")
.withUsername(POSTGRES_USERNAME)
.withPassword(POSTGRES_PASSWORD);
TestContainers
https://github.com/testcontainers/testcontainers-java
Docker Compose? - Yes!
public static DockerComposeContainer env =
new DockerComposeContainer(
new File("src/test/resources/compose-test.yml"))
.withExposedService("redis_1", REDIS_PORT);
Docker Compose? - Yes!
public static DockerComposeContainer env =
new DockerComposeContainer(
new File("src/test/resources/compose-test.yml"))
.withExposedService("redis_1", REDIS_PORT);
String url = env.getServiceHost("redis_1", REDIS_PORT);
Integer port = env.getServicePort("redis_1", REDIS_PORT);
Jedis jedis = new Jedis(url, port);
Microservices!11
REST service
Java, Spring Boot
Redis & PostgreSQL
Calls some other micro-services
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class,
webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = AbstractIntegrationTest.Initializer.class)
public abstract class AbstractIntegrationTest {
@ClassRule
public static PostgreSQLContainer postgreSql = new PostgreSQLContainer();
@ClassRule
public static GenericContainer redis = new GenericContainer("redis:3.0.6")
.withExposedPorts(6379);
@ClassRule
public static MockServerContainer mockServer = new MockServerContainer();
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class,
webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = AbstractIntegrationTest.Initializer.class)
public abstract class AbstractIntegrationTest {
@ClassRule
public static PostgreSQLContainer postgreSql = new PostgreSQLContainer();
@ClassRule
public static GenericContainer redis = new GenericContainer("redis:3.0.6")
.withExposedPorts(6379);
@ClassRule
public static MockServerContainer mockServer = new MockServerContainer();
Still using all the
Spring goodies!
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class,
webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = AbstractIntegrationTest.Initializer.class)
public abstract class AbstractIntegrationTest {
@ClassRule
public static PostgreSQLContainer postgreSql = new PostgreSQLContainer();
@ClassRule
public static GenericContainer redis = new GenericContainer("redis:3.0.6")
.withExposedPorts(6379);
@ClassRule
public static MockServerContainer mockServer = new MockServerContainer();
External
dependencies
public class MockServerContainer<SELF extends MockServerContainer<SELF>>
extends GenericContainer<SELF> {
public MockServerContainer() {
super("jamesdbloom/mockserver:latest");
addExposedPorts(8080);
}
private MockServerClient client;
@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
super.containerIsStarted(containerInfo);
client = new MockServerClient(getContainerIpAddress(),
getMappedPort(getExposedPorts().get(0)));
}
}
public class MockServerContainer<SELF extends MockServerContainer<SELF>>
extends GenericContainer<SELF> {
public MockServerContainer() {
super("jamesdbloom/mockserver:latest");
addExposedPorts(8080);
}
private MockServerClient client;
@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
super.containerIsStarted(containerInfo);
client = new MockServerClient(getContainerIpAddress(),
getMappedPort(getExposedPorts().get(0)));
}
}
MockServerClient client = …
client.when(
HttpRequest.request("/analytics/issues/" +
project.projectId + "/app").withMethod("GET"),
Times.unlimited())
.respond(singleIssueResponse());
How to test Java agents?
public static void premain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
if ("spark/webserver/JettyHandler".equals(className)) {
try {
ClassPool cp = new ClassPool();
cp.appendClassPath(new LoaderClassPath(loader));
CtClass ct = cp.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod ctMethod = ct.getDeclaredMethod("doHandle");
ctMethod.insertBefore("{ $4.setHeader("X-My-Super-Header", "header value"); }");
return ct.toBytecode();
} catch (Throwable e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
});
}
public static void premain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
if ("spark/webserver/JettyHandler".equals(className)) {
try {
ClassPool cp = new ClassPool();
cp.appendClassPath(new LoaderClassPath(loader));
CtClass ct = cp.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod ctMethod = ct.getDeclaredMethod("doHandle");
ctMethod.insertBefore("{ $4.setHeader("X-My-Super-Header", "header value"); }");
return ct.toBytecode();
} catch (Throwable e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
});
}
public static void premain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
if ("spark/webserver/JettyHandler".equals(className)) {
try {
ClassPool cp = new ClassPool();
cp.appendClassPath(new LoaderClassPath(loader));
CtClass ct = cp.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod ctMethod = ct.getDeclaredMethod("doHandle");
ctMethod.insertBefore("{ $4.setHeader("X-My-Super-Header", "header value"); }");
return ct.toBytecode();
} catch (Throwable e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
});
}
public class AgentTest {
@ClassRule
public static BasicTestApp app = new BasicTestApp();
@Test
public void testIt() throws Exception {
Response response = app.getClient().getHello();
System.out.println("Got response:n" + response);
assertThat(response.headers().get("X-My-Super-Header"))
.isNotNull()
.hasSize(1)
.containsExactly("header value");
}
}
public class AgentTest {
@ClassRule
public static BasicTestApp app = new BasicTestApp();
@Test
public void testIt() throws Exception {
Response response = app.getClient().getHello();
System.out.println("Got response:n" + response);
assertThat(response.headers().get("X-My-Super-Header"))
.isNotNull()
.hasSize(1)
.containsExactly("header value");
}
}
A custom
container
public class AgentTest {
@ClassRule
public static BasicTestApp app = new BasicTestApp();
@Test
public void testIt() throws Exception {
Response response = app.getClient().getHello();
System.out.println("Got response:n" + response);
assertThat(response.headers().get("X-My-Super-Header"))
.isNotNull()
.hasSize(1)
.containsExactly("header value");
}
}
Verify, if the
new behaviour is
there
https://github.com/bsideup/javaagent-boilerplate
https://www.testcontainers.org
https://twitter.com/testcontainers
https://github.com/testcontainers
https://testcontainers.slack.com
anton@zeroturnaround.com
@antonarhipov
slideshare.net/arhan

JavaOne 2017 - TestContainers: integration testing without the hassle

  • 1.
  • 3.
  • 9.
    1. # oftests per time unit
  • 10.
    1. # oftests per time unit 2. Complex setup
  • 12.
  • 13.
    1. Reproducible environment 2.Both for development and CI
  • 14.
    1. Reproducible environment 2.Both for development and CI 3. Isolated
  • 15.
    1. Reproducible environment 2.Both for development and CI 3. Isolated 4. As real as possible
  • 16.
    1. Reproducible environment 2.Both for development and CI 3. Isolated 4. As real as possible 5. Cross-platform
  • 17.
    1. Reproducible environment 2.Both for development and CI 3. Isolated 4. As real as possible 5. Cross-platform 6. Easy to set up, use & maintain
  • 19.
    Docker Compose FTW! redis:
 image:redis
 ports:
 - "6379:6379"
 postgres:
 image: postgres
 ports:
 - "5432:5432"
 elasticsearch:
 image: elasticsearch:5.0.0
 ports:
 - "9200:9200"

  • 24.
  • 25.
    How does itwork? https://github.com/docker-java/docker-java Docker environment discovery Will start docker machine if it’s not started yet Containers cleanup on JVM shutdown
  • 26.
    GenericContainer redis = newGenericContainer("redis:3.0.6") .withExposedPorts(6379);
  • 27.
    GenericContainer redis = newGenericContainer("redis:3.0.6") .withExposedPorts(6379); PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:9.6.2") .withUsername(POSTGRES_USERNAME) .withPassword(POSTGRES_PASSWORD);
  • 28.
  • 29.
    Docker Compose? -Yes! public static DockerComposeContainer env = new DockerComposeContainer( new File("src/test/resources/compose-test.yml")) .withExposedService("redis_1", REDIS_PORT);
  • 30.
    Docker Compose? -Yes! public static DockerComposeContainer env = new DockerComposeContainer( new File("src/test/resources/compose-test.yml")) .withExposedService("redis_1", REDIS_PORT); String url = env.getServiceHost("redis_1", REDIS_PORT); Integer port = env.getServicePort("redis_1", REDIS_PORT); Jedis jedis = new Jedis(url, port);
  • 31.
    Microservices!11 REST service Java, SpringBoot Redis & PostgreSQL Calls some other micro-services
  • 32.
    @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = DemoApplication.class, webEnvironment= WebEnvironment.RANDOM_PORT) @ContextConfiguration(initializers = AbstractIntegrationTest.Initializer.class) public abstract class AbstractIntegrationTest { @ClassRule public static PostgreSQLContainer postgreSql = new PostgreSQLContainer(); @ClassRule public static GenericContainer redis = new GenericContainer("redis:3.0.6") .withExposedPorts(6379); @ClassRule public static MockServerContainer mockServer = new MockServerContainer();
  • 33.
    @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = DemoApplication.class, webEnvironment= WebEnvironment.RANDOM_PORT) @ContextConfiguration(initializers = AbstractIntegrationTest.Initializer.class) public abstract class AbstractIntegrationTest { @ClassRule public static PostgreSQLContainer postgreSql = new PostgreSQLContainer(); @ClassRule public static GenericContainer redis = new GenericContainer("redis:3.0.6") .withExposedPorts(6379); @ClassRule public static MockServerContainer mockServer = new MockServerContainer(); Still using all the Spring goodies!
  • 34.
    @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = DemoApplication.class, webEnvironment= WebEnvironment.RANDOM_PORT) @ContextConfiguration(initializers = AbstractIntegrationTest.Initializer.class) public abstract class AbstractIntegrationTest { @ClassRule public static PostgreSQLContainer postgreSql = new PostgreSQLContainer(); @ClassRule public static GenericContainer redis = new GenericContainer("redis:3.0.6") .withExposedPorts(6379); @ClassRule public static MockServerContainer mockServer = new MockServerContainer(); External dependencies
  • 36.
    public class MockServerContainer<SELFextends MockServerContainer<SELF>> extends GenericContainer<SELF> { public MockServerContainer() { super("jamesdbloom/mockserver:latest"); addExposedPorts(8080); } private MockServerClient client; @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { super.containerIsStarted(containerInfo); client = new MockServerClient(getContainerIpAddress(), getMappedPort(getExposedPorts().get(0))); } }
  • 37.
    public class MockServerContainer<SELFextends MockServerContainer<SELF>> extends GenericContainer<SELF> { public MockServerContainer() { super("jamesdbloom/mockserver:latest"); addExposedPorts(8080); } private MockServerClient client; @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { super.containerIsStarted(containerInfo); client = new MockServerClient(getContainerIpAddress(), getMappedPort(getExposedPorts().get(0))); } }
  • 38.
    MockServerClient client =… client.when( HttpRequest.request("/analytics/issues/" + project.projectId + "/app").withMethod("GET"), Times.unlimited()) .respond(singleIssueResponse());
  • 41.
    How to testJava agents?
  • 42.
    public static voidpremain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if ("spark/webserver/JettyHandler".equals(className)) { try { ClassPool cp = new ClassPool(); cp.appendClassPath(new LoaderClassPath(loader)); CtClass ct = cp.makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod ctMethod = ct.getDeclaredMethod("doHandle"); ctMethod.insertBefore("{ $4.setHeader("X-My-Super-Header", "header value"); }"); return ct.toBytecode(); } catch (Throwable e) { e.printStackTrace(); } } return classfileBuffer; } }); }
  • 43.
    public static voidpremain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if ("spark/webserver/JettyHandler".equals(className)) { try { ClassPool cp = new ClassPool(); cp.appendClassPath(new LoaderClassPath(loader)); CtClass ct = cp.makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod ctMethod = ct.getDeclaredMethod("doHandle"); ctMethod.insertBefore("{ $4.setHeader("X-My-Super-Header", "header value"); }"); return ct.toBytecode(); } catch (Throwable e) { e.printStackTrace(); } } return classfileBuffer; } }); }
  • 44.
    public static voidpremain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if ("spark/webserver/JettyHandler".equals(className)) { try { ClassPool cp = new ClassPool(); cp.appendClassPath(new LoaderClassPath(loader)); CtClass ct = cp.makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod ctMethod = ct.getDeclaredMethod("doHandle"); ctMethod.insertBefore("{ $4.setHeader("X-My-Super-Header", "header value"); }"); return ct.toBytecode(); } catch (Throwable e) { e.printStackTrace(); } } return classfileBuffer; } }); }
  • 45.
    public class AgentTest{ @ClassRule public static BasicTestApp app = new BasicTestApp(); @Test public void testIt() throws Exception { Response response = app.getClient().getHello(); System.out.println("Got response:n" + response); assertThat(response.headers().get("X-My-Super-Header")) .isNotNull() .hasSize(1) .containsExactly("header value"); } }
  • 46.
    public class AgentTest{ @ClassRule public static BasicTestApp app = new BasicTestApp(); @Test public void testIt() throws Exception { Response response = app.getClient().getHello(); System.out.println("Got response:n" + response); assertThat(response.headers().get("X-My-Super-Header")) .isNotNull() .hasSize(1) .containsExactly("header value"); } } A custom container
  • 47.
    public class AgentTest{ @ClassRule public static BasicTestApp app = new BasicTestApp(); @Test public void testIt() throws Exception { Response response = app.getClient().getHello(); System.out.println("Got response:n" + response); assertThat(response.headers().get("X-My-Super-Header")) .isNotNull() .hasSize(1) .containsExactly("header value"); } } Verify, if the new behaviour is there
  • 48.
  • 49.
  • 50.