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

Anton Arhipov

May 19, 2017
Tweet

More Decks by Anton Arhipov

Other Decks in Technology

Transcript

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. Docker Compose FTW! redis:
 image: redis
 ports:
 - "6379:6379"
 postgres:


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

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


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

  9. 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
  10. 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);
  11. 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);
  12. Testing of microservices REST service Java, Spring Boot Redis and

    PostgreSQL Calls some other micro-services
  13. @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!
  14. @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
  15. @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
  16. 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))); } }
  17. 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))); } }
  18. @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;
  19. @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);
  20. 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
  21. 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
  22. 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; } }); }
  23. 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; } }); }
  24. 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
  25. 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