Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
TestContainers
Integration testing without the hassle
@antonarhipov
whoami
Anton Arhipov
@antonarhipov
Integration testing.
Why it matters?
Integrated
tests are a
scam!
J.B. Reinsberger
http://jbrains.ca
Verify how your software product will behave in real-world conditions
Real:
databases,
file systems,
network interfaces, …
let me
tell you a
story…
A typical JRebel test
1. Start application server (Tomcat, JBoss, etc)
A typical JRebel test
1. Start application server (Tomcat, JBoss, etc)
2. Deploy v1 of web application
A typical JRebel test
1. Start application server (Tomcat, JBoss, etc)
2. Deploy v1 of web application
3. Verify the behaviour
A typical JRebel ...
1. Start application server (Tomcat, JBoss, etc)
2. Deploy v1 of web application
3. Verify the behaviour
4. Compile v2 of ...
1. Start application server (Tomcat, JBoss, etc)
2. Deploy v1 of web application
3. Verify the behaviour
4. Compile v2 of ...
2. Deploy v1 of web application
3. Verify the behaviour
4. Compile v2 of the same application
5. Verify the new behaviour
...
3. Verify the behaviour
4. Compile v2 of the same application
5. Verify the new behaviour
6. Compile v3 of this applicatio...
4. Compile v2 of the same application
5. Verify the new behaviour
6. Compile v3 of this application
7. Verify again
8. ???...
5. Verify the new behaviour
6. Compile v3 of this application
7. Verify again
8. ???
9. Stop the application server & clea...
The requirements
The requirements
1. Reproducible environment
The requirements
1. Reproducible environment
2. Both for development and CI
The requirements
1. Reproducible environment
2. Both for development and CI
3. Isolated
The requirements
1. Reproducible environment
2. Both for development and CI
3. Isolated
4. As real as possible
The requirements
1. Reproducible environment
2. Both for development and CI
3. Isolated
4. As real as possible
5. Cross-pl...
The requirements
1. Reproducible environment
2. Both for development and CI
3. Isolated
4. As real as possible
5. Cross-pl...
Docker Compose FTW!
redis:

image: redis

ports:

- "6379:6379"

postgres:

image: postgres

ports:

- "5432:5432"

elasti...
No ports randomization :(
redis:

image: redis

ports:

- "6379:6379"

postgres:

image: postgres

ports:

- "5432:5432"

...
Fighting with Docker environment
How does it work?
Wraps https://github.com/docker-java/docker-java
Docker environment discovery
Will start docker-machine ...
As simple as
GenericContainer redis =
new GenericContainer("redis:3.0.6")
.withExposedPorts(6379);
As simple as
GenericContainer redis =
new GenericContainer("redis:3.0.6")
.withExposedPorts(6379);
PostgreSQLContainer pos...
https://github.com/testcontainers/testcontainers-java
Docker Compose? - Yes!
public static DockerComposeContainer env =
new DockerComposeContainer(
new File("src/test/resources...
Docker Compose? - Yes!
public static DockerComposeContainer env =
new DockerComposeContainer(
new File("src/test/resources...
good job!
show me more!
Testing of microservices
REST service
Java, Spring Boot
Redis and PostgreSQL
Calls some other micro-services
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class, webEnvironment = WebEnvironment.R...
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class, webEnvironment = WebEnvironment.R...
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class, webEnvironment = WebEnvironment.R...
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class, webEnvironment = WebEnvironment.R...
public class MockServerContainer<SELF extends MockServerContainer<SELF>>
extends GenericContainer<SELF> {
public MockServe...
public class MockServerContainer<SELF extends MockServerContainer<SELF>>
extends GenericContainer<SELF> {
public MockServe...
Isolated from other system components to avoid false negatives
I applaude!
I applaude!
Docker as Selenium driver
Selenium/Selenide tests
No need to install Chrome/Firefox/etc
CI friendly
@Rule
public BrowserWebDriverContainer chrome =
new BrowserWebDriverContainer()
.withDesiredCapabilities(DesiredCapabiliti...
@Rule
public BrowserWebDriverContainer chrome =
new BrowserWebDriverContainer()
.withDesiredCapabilities(DesiredCapabiliti...
@Rule
public BrowserWebDriverContainer chrome =
new BrowserWebDriverContainer()
.withDesiredCapabilities(DesiredCapabiliti...
@Rule
public BrowserWebDriverContainer chrome =
new BrowserWebDriverContainer()
.withDesiredCapabilities(DesiredCapabiliti...
NOT BAD
NOT BAD
NOT BAD AT ALL!
Java agent testing
Test different Java versions
Simplify I/O testing
The actual reason why ZeroTurnaround uses TestContaine...
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class Agent {
public...
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class Agent {
public...
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class Agent {
public...
public static void premain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer(new ClassFileTra...
public static void premain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer(new ClassFileTra...
public static void premain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer(new ClassFileTra...
public class AgentTest {
@ClassRule
public static BasicTestApp app = new BasicTestApp();
@Test
public void testIt() throws...
public class AgentTest {
@ClassRule
public static BasicTestApp app = new BasicTestApp();
@Test
public void testIt() throws...
public class AgentTest {
@ClassRule
public static BasicTestApp app = new BasicTestApp();
@Test
public void testIt() throws...
https://github.com/bsideup/javaagent-boilerplate
chuck norris approves
chuck norris approves
anton@zeroturnaround.com
@antonarhipov
GeeCON 2017 - TestContainers. Integration testing without the hassle
GeeCON 2017 - TestContainers. Integration testing without the hassle
GeeCON 2017 - TestContainers. Integration testing without the hassle
GeeCON 2017 - TestContainers. Integration testing without the hassle
GeeCON 2017 - TestContainers. Integration testing without the hassle
GeeCON 2017 - TestContainers. Integration testing without the hassle
GeeCON 2017 - TestContainers. Integration testing without the hassle
Upcoming SlideShare
Loading in …5
×

GeeCON 2017 - TestContainers. Integration testing without the hassle

1,925 views

Published on

TestContainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.

Published in: Technology
  • Be the first to comment

  • Be the first to like this

GeeCON 2017 - TestContainers. Integration testing without the hassle

  1. 1. TestContainers Integration testing without the hassle @antonarhipov
  2. 2. whoami Anton Arhipov @antonarhipov
  3. 3. Integration testing. Why it matters?
  4. 4. Integrated tests are a scam! J.B. Reinsberger http://jbrains.ca
  5. 5. Verify how your software product will behave in real-world conditions
  6. 6. Real: databases, file systems, network interfaces, …
  7. 7. let me tell you a story…
  8. 8. A typical JRebel test
  9. 9. 1. Start application server (Tomcat, JBoss, etc) A typical JRebel test
  10. 10. 1. Start application server (Tomcat, JBoss, etc) 2. Deploy v1 of web application A typical JRebel test
  11. 11. 1. Start application server (Tomcat, JBoss, etc) 2. Deploy v1 of web application 3. Verify the behaviour A typical JRebel test
  12. 12. 1. Start application server (Tomcat, JBoss, etc) 2. Deploy v1 of web application 3. Verify the behaviour 4. Compile v2 of the same application A typical JRebel test
  13. 13. 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
  14. 14. 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
  15. 15. 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
  16. 16. 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
  17. 17. 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
  18. 18. The requirements
  19. 19. The requirements 1. Reproducible environment
  20. 20. The requirements 1. Reproducible environment 2. Both for development and CI
  21. 21. The requirements 1. Reproducible environment 2. Both for development and CI 3. Isolated
  22. 22. The requirements 1. Reproducible environment 2. Both for development and CI 3. Isolated 4. As real as possible
  23. 23. The requirements 1. Reproducible environment 2. Both for development and CI 3. Isolated 4. As real as possible 5. Cross-platform
  24. 24. 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
  25. 25. Docker Compose FTW! redis:
 image: redis
 ports:
 - "6379:6379"
 postgres:
 image: postgres
 ports:
 - "5432:5432"
 elasticsearch:
 image: elasticsearch:5.0.0
 ports:
 - "9200:9200"

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

  27. 27. Fighting with Docker environment
  28. 28. 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
  29. 29. As simple as GenericContainer redis = new GenericContainer("redis:3.0.6") .withExposedPorts(6379);
  30. 30. 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);
  31. 31. https://github.com/testcontainers/testcontainers-java
  32. 32. Docker Compose? - Yes! public static DockerComposeContainer env = new DockerComposeContainer( new File("src/test/resources/compose-test.yml")) .withExposedService("redis_1", REDIS_PORT);
  33. 33. 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);
  34. 34. good job! show me more!
  35. 35. Testing of microservices REST service Java, Spring Boot Redis and PostgreSQL Calls some other micro-services
  36. 36. @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) ); } } }
  37. 37. @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!
  38. 38. @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
  39. 39. @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
  40. 40. 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))); } }
  41. 41. 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))); } }
  42. 42. Isolated from other system components to avoid false negatives
  43. 43. I applaude!
  44. 44. I applaude!
  45. 45. Docker as Selenium driver Selenium/Selenide tests No need to install Chrome/Firefox/etc CI friendly
  46. 46. @Rule public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer() .withDesiredCapabilities(DesiredCapabilities.chrome()) .withRecordingMode(RECORD_ALL, new File("target"));
  47. 47. @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);
  48. 48. @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;
  49. 49. @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);
  50. 50. NOT BAD
  51. 51. NOT BAD NOT BAD AT ALL!
  52. 52. 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
  53. 53. 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 }); } }
  54. 54. 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 }); } } META-INF/MANIFEST.MF Premain-Class: Agent
  55. 55. 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
  56. 56. 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; } }); }
  57. 57. 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; } }); }
  58. 58. 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; } }); }
  59. 59. 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"); } }
  60. 60. 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
  61. 61. 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
  62. 62. https://github.com/bsideup/javaagent-boilerplate
  63. 63. chuck norris approves
  64. 64. chuck norris approves
  65. 65. anton@zeroturnaround.com @antonarhipov

×