Slide 1

Slide 1 text

TestContainers Integration testing without the hassle @antonarhipov

Slide 2

Slide 2 text

whoami Anton Arhipov @antonarhipov

Slide 3

Slide 3 text

Integration testing. Why it matters?

Slide 4

Slide 4 text

Integrated tests are a scam! J.B. Reinsberger http://jbrains.ca

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

Verify how your software product will behave in real-world conditions

Slide 8

Slide 8 text

Real: databases, file systems, network interfaces, …

Slide 9

Slide 9 text

let me tell you a story…

Slide 10

Slide 10 text

1. Start application server (Tomcat, JBoss, etc) 2. Deploy v1 of web application 3. Verify the behaviour 4. Compile v2 of the same application 5. Verify the new behaviour A typical JRebel test

Slide 11

Slide 11 text

2. Deploy v1 of web application 3. Verify the behaviour 4. Compile v2 of the same application 5. Verify the new behaviour 6. Compile v3 of this application A typical JRebel test

Slide 12

Slide 12 text

3. Verify the behaviour 4. Compile v2 of the same application 5. Verify the new behaviour 6. Compile v3 of this application 7. Verify again A typical JRebel test

Slide 13

Slide 13 text

4. Compile v2 of the same application 5. Verify the new behaviour 6. Compile v3 of this application 7. Verify again 8. ??? A typical JRebel test

Slide 14

Slide 14 text

5. Verify the new behaviour 6. Compile v3 of this application 7. Verify again 8. ??? 9. Stop the application server & clean up A typical JRebel test

Slide 15

Slide 15 text

The requirements 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

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 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"


Slide 18

Slide 18 text

No ports randomization :( redis:
 image: redis
 ports:
 - "6379:6379"
 postgres:
 image: postgres
 ports:
 - "5432:5432"
 elasticsearch:
 image: elasticsearch:5.0.0
 ports:
 - "9200:9200"


Slide 19

Slide 19 text

Fighting with Docker environment

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

How does it work? Wraps 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

Slide 22

Slide 22 text

As simple as GenericContainer redis = new GenericContainer("redis:3.0.6") .withExposedPorts(6379); PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:9.6.2") .withUsername(POSTGRES_USERNAME) .withPassword(POSTGRES_PASSWORD);

Slide 23

Slide 23 text

https://github.com/testcontainers/testcontainers-java

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

good job! show me more!

Slide 26

Slide 26 text

Testing of microservices REST service Java, Spring Boot Redis and PostgreSQL Calls some other micro-services

Slide 27

Slide 27 text

@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(); public static class Initializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext configurableApplicationContext) { EnvironmentTestUtils.addEnvironment(configurableApplicationContext, "spring.database.url=" + postgreSql.getJdbcUrl(), "spring.database.username=" + postgreSql.getUsername(), "spring.database.password=" + postgreSql.getPassword(), "spring.redis.host=" + redis.getContainerIpAddress(), "spring.redis.port=" + redis.getMappedPort(6379), "spring.redis.port=" + mockServer.getContainerIpAddress() + ":" + mockServer.getMappedPort(8080) ); } } } Still using all the Spring goodies!

Slide 28

Slide 28 text

@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(); public static class Initializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext configurableApplicationContext) { EnvironmentTestUtils.addEnvironment(configurableApplicationContext, "spring.database.url=" + postgreSql.getJdbcUrl(), "spring.database.username=" + postgreSql.getUsername(), "spring.database.password=" + postgreSql.getPassword(), "spring.redis.host=" + redis.getContainerIpAddress(), "spring.redis.port=" + redis.getMappedPort(6379), "spring.redis.port=" + mockServer.getContainerIpAddress() + ":" + mockServer.getMappedPort(8080) ); } } } External dependencies

Slide 29

Slide 29 text

@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(); public static class Initializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext configurableApplicationContext) { EnvironmentTestUtils.addEnvironment(configurableApplicationContext, "spring.database.url=" + postgreSql.getJdbcUrl(), "spring.database.username=" + postgreSql.getUsername(), "spring.database.password=" + postgreSql.getPassword(), "spring.redis.host=" + redis.getContainerIpAddress(), "spring.redis.port=" + redis.getMappedPort(6379), "spring.redis.port=" + mockServer.getContainerIpAddress() + ":" + mockServer.getMappedPort(8080) ); } } } Setting up Spring Boot environment

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

public class MockServerContainer> extends GenericContainer { 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))); } }

Slide 32

Slide 32 text

public class MockServerContainer> extends GenericContainer { 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))); } }

Slide 33

Slide 33 text

Isolated from other system components to avoid false negatives

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

Docker as Selenium driver Selenium/Selenide tests No need to install Chrome/Firefox/etc CI friendly

Slide 36

Slide 36 text

@Rule public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer() .withDesiredCapabilities(DesiredCapabilities.chrome()) .withRecordingMode(RECORD_ALL, new File("target"));

Slide 37

Slide 37 text

@Rule public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer() .withDesiredCapabilities(DesiredCapabilities.chrome()) .withRecordingMode(RECORD_ALL, new File("target")); RemoteWebDriver driver = chrome.getWebDriver(); driver.get("https://wikipedia.org"); WebElement searchInput = driver.findElementByName("search"); searchInput.sendKeys("Rick Astley"); searchInput.submit(); WebElement otherPage = driver.findElementByLinkText("Rickrolling"); otherPage.click(); boolean expectedTextFound = driver.findElementsByCssSelector("p") .stream() .anyMatch(element -> element.getText().contains("meme")); assertTrue("The word 'meme' is found on a page about rickrolling", expectedTextFound); import org.openqa.selenium.remote.RemoteWebDriver;

Slide 38

Slide 38 text

@Rule public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer() .withDesiredCapabilities(DesiredCapabilities.chrome()) .withRecordingMode(RECORD_ALL, new File("target")); RemoteWebDriver driver = chrome.getWebDriver(); driver.get("https://wikipedia.org"); WebElement searchInput = driver.findElementByName("search"); searchInput.sendKeys("Rick Astley"); searchInput.submit(); WebElement otherPage = driver.findElementByLinkText("Rickrolling"); otherPage.click(); boolean expectedTextFound = driver.findElementsByCssSelector("p") .stream() .anyMatch(element -> element.getText().contains("meme")); assertTrue("The word 'meme' is found on a page about rickrolling", expectedTextFound);

Slide 39

Slide 39 text

NOT BAD NOT BAD AT ALL!

Slide 40

Slide 40 text

Java agent testing Test different Java versions Simplify I/O testing The actual reason why ZeroTurnaround uses TestContainers :) DEMO https://github.com/bsideup/javaagent-boilerplate

Slide 41

Slide 41 text

import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; public class Agent { public static void premain(String args, Instrumentation inst) throws Exception { inst.addTransformer(new ClassFileTransformer { // here be dragons }); } } $> java –javaagent:agent.jar application.Main META-INF/MANIFEST.MF Premain-Class: Agent

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

https://github.com/bsideup/javaagent-boilerplate

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

[email protected] @antonarhipov