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

DevoxxPL 2017 - TestContainers. Integration testing without hassle

DevoxxPL 2017 - TestContainers. Integration testing without hassle

1bc80e2eee2adeaa8bb577798d92e9d0?s=128

Anton Arhipov

June 22, 2017
Tweet

Transcript

  1. @antonarhipov TestContainers Integration testing without hassle

  2. whoami Anton Arhipov @antonarhipov

  3. Integration testing. Why it matters?

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

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

  9. None
  10. 1. Reproducible environment

  11. 1. Reproducible environment 2. Both for development and CI

  12. 1. Reproducible environment 2. Both for development and CI 3.

    Isolated
  13. 1. Reproducible environment 2. Both for development and CI 3.

    Isolated 4. As real as possible
  14. 1. Reproducible environment 2. Both for development and CI 3.

    Isolated 4. As real as possible 5. Cross-platform
  15. 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);

  23. 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);
  24. https://github.com/testcontainers/testcontainers-java

  25. Docker Compose? - Yes! public static DockerComposeContainer env = new

    DockerComposeContainer( new File("src/test/resources/compose-test.yml")) .withExposedService("redis_1", REDIS_PORT);
  26. 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);
  27. Testing of microservices REST service Java, Spring Boot Redis and

    PostgreSQL Calls some other micro-services
  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();
  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(); Still using all the Spring goodies!
  30. @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
  31. None
  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. 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))); } }
  34. Isolated from other system components to avoid false negatives

  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);
  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); import org.openqa.selenium.remote.RemoteWebDriver;
  39. @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);
  40. https://github.com/selenide-examples/cucumber

  41. 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
  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. 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; } }); }
  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. https://github.com/bsideup/javaagent-boilerplate

  49. anton@zeroturnaround.com @antonarhipov