Upgrade to Pro — share decks privately, control downloads, hide ads and more …

GeeCON 2017 - TestContainers. Integration testing without hassle

GeeCON 2017 - TestContainers. Integration testing without hassle

1bc80e2eee2adeaa8bb577798d92e9d0?s=128

Anton Arhipov

May 19, 2017
Tweet

Transcript

  1. TestContainers Integration testing without the hassle @antonarhipov

  2. whoami Anton Arhipov @antonarhipov

  3. Integration testing. Why it matters?

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

  5. None
  6. None
  7. Verify how your software product will behave in real-world conditions

  8. Real: databases, file systems, network interfaces, …

  9. let me tell you a story…

  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. None
  17. Docker Compose FTW! redis:
 image: redis
 ports:
 - "6379:6379"
 postgres:


    image: postgres
 ports:
 - "5432:5432"
 elasticsearch:
 image: elasticsearch:5.0.0
 ports:
 - "9200:9200"

  18. No ports randomization :( redis:
 image: redis
 ports:
 - "6379:6379"


    postgres:
 image: postgres
 ports:
 - "5432:5432"
 elasticsearch:
 image: elasticsearch:5.0.0
 ports:
 - "9200:9200"

  19. Fighting with Docker environment

  20. None
  21. 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
  22. 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);
  23. https://github.com/testcontainers/testcontainers-java

  24. 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);
  25. good job! show me more!

  26. Testing of microservices REST service Java, Spring Boot Redis and

    PostgreSQL Calls some other micro-services
  27. @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<ConfigurableApplicationContext> { @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!
  28. @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<ConfigurableApplicationContext> { @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
  29. @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<ConfigurableApplicationContext> { @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
  30. None
  31. 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))); } }
  32. 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))); } }
  33. Isolated from other system components to avoid false negatives

  34. None
  35. Docker as Selenium driver Selenium/Selenide tests No need to install

    Chrome/Firefox/etc CI friendly
  36. @Rule public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer() .withDesiredCapabilities(DesiredCapabilities.chrome()) .withRecordingMode(RECORD_ALL, new

    File("target"));
  37. @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;
  38. @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);
  39. NOT BAD NOT BAD AT ALL!

  40. 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
  41. 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
  42. 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; } }); }
  43. 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; } }); }
  44. None
  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"); } } A custom container
  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"); } } Verify, if the new behaviour is there
  47. https://github.com/bsideup/javaagent-boilerplate

  48. None
  49. anton@zeroturnaround.com @antonarhipov