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

Consumer Driven Contracts

Pedro Tavares
September 28, 2017
1.1k

Consumer Driven Contracts

Pedro Tavares

September 28, 2017
Tweet

Transcript

  1. END-TO-END-TESTS build test deploy build test deploy build test deploy

    microservices END-TO-END Hard to write and maintain, slow to execute, and late feedback a b c
  2. What is wrong with this? Slow to run Very late

    feedback Hard to write and maintain Mocked stubs can be different from the reality Leads to passing tests and failing in production
  3. What is wrong with this? Slow to run Very late

    feedback Hard to write and maintain Mocked stubs can be different from the reality Leads to passing tests and failing in production
  4. What is wrong with this? Slow to run Very late

    feedback Hard to write and maintain Mocked stubs can be different from the reality Leads to passing tests and failing in production
  5. What is wrong with this? Slow to run Very late

    feedback Hard to write and maintain Mocked stubs can be different from the reality Leads to passing tests and failing in production
  6. What is wrong with this? Slow to run Very late

    feedback Hard to write and maintain Mocked stubs can be different from the reality Leads to passing tests and failing in production
  7. # Check if a person with a given age can

    drink beer curl -H "Content-Type: application/json" -X POST \ -d ‘{ “age”: 20 }' \ http://some.external.api:8090/check ~ provided by the producer at some point of time REAL LIFE EXAMPLE This snippet was taken from JIRA or some random Slack channel
  8. @Test public void check_withValidAge_shouldReturnHttpStatusOk() throws Exception { ResponseEntity<ResponsePayload> response =

    restTemplate.exchange( RequestEntity .put(URI.create("http://localhost:8080/chekc")) .contentType(MediaType.APPLICATION_JSON_UTF8) .body(new RequestPayload(10)), ResponsePayload.class); assertThat(response.getStatusCode()).isEqualByComparingTo(HttpStatus.OK); } TESTING on the consumer side
  9. @Test public void check_withValidAge_shouldReturnHttpStatusOk() throws Exception { ResponseEntity<ResponsePayload> response =

    restTemplate.exchange( RequestEntity .put(URI.create("http://localhost:8091/items/validate")) .contentType(MediaType.APPLICATION_JSON_UTF8) .body(new RequestPayload(10)), ResponsePayload.class); assertThat(response.getStatusCode()).isEqualByComparingTo(HttpStatus.OK); } check_withValidAge_shouldReturnHttpStatusOk OK TESTING on the consumer side
  10. @Test public void check_withValidAge_shouldReturnHttpStatusOk() throws Exception { ResponseEntity<ResponsePayload> response =

    restTemplate.exchange( RequestEntity .put(URI.create("http://localhost:8080/chekc")) .contentType(MediaType.APPLICATION_JSON_UTF8) .body(new RequestPayload(10)), ResponsePayload.class); assertThat(response.getStatusCode()).isEqualByComparingTo(HttpStatus.OK); } TESTING on the consumer side
  11. @Test public void check_withValidAge_shouldReturnHttpStatusOk() throws Exception { ResponseEntity<ResponsePayload> response =

    restTemplate.exchange( RequestEntity .put(URI.create("http://localhost:8080/chekc")) .contentType(MediaType.APPLICATION_JSON_UTF8) .body(new RequestPayload(10)), ResponsePayload.class); assertThat(response.getStatusCode()).isEqualByComparingTo(HttpStatus.OK); } TESTING on the consumer side
  12. @Test public void check_withValidAge_shouldReturnHttpStatusOk() throws Exception { ResponseEntity<ResponsePayload> response =

    restTemplate.exchange( RequestEntity .put(URI.create("http://localhost:8080/chekc")) .contentType(MediaType.APPLICATION_JSON_UTF8) .body(new RequestPayload(10)), ResponsePayload.class); assertThat(response.getStatusCode()).isEqualByComparingTo(HttpStatus.OK); } TESTING on the consumer side
  13. @Test public void check_withValidAge_shouldReturnHttpStatusOk() throws Exception { ResponseEntity<ResponsePayload> response =

    restTemplate.exchange( RequestEntity .put(URI.create("http://localhost:8080/chekc")) .contentType(MediaType.APPLICATION_JSON_UTF8) .body(new RequestPayload(10)), ResponsePayload.class); assertThat(response.getStatusCode()).isEqualByComparingTo(HttpStatus.OK); } TESTING on the consumer side
  14. “An integration contract test is a test at the boundary

    of an external service verifying that it meets the contract expected by a consuming service” Martin Fowler Contract testing?
  15. “An integration contract test is a test at the boundary

    of an external service verifying that it meets the contract expected by a consuming service” Martin Fowler Contract testing?
  16. Contract.make { request { method "POST" url "/check" body ([

    age: value(regex("[2-9][0-9]")) ]) headers { contentType(applicationJson()) } } response { status 200 body([ status: "OK" ]) headers { contentType(applicationJson()) } } } defining the contract
  17. Contract.make { request { method "POST" url "/check" body ([

    age: value(regex("[2-9][0-9]")) ]) headers { contentType(applicationJson()) } } response { status 200 body([ status: "OK" ]) headers { contentType(applicationJson()) } } } defining the contract
  18. @Test public void validate_validSizeShouldReturnHttpOk() throws Exception { // given: MockMvcRequestSpecification

    request = given() .header("Content-Type", "application/json") .body("{\"age\":\"20\"}"); // when: ResponseOptions response = given().spec(request) .post("/check"); // then: assertThat(response.statusCode()).isEqualTo(200); assertThat(response.header("Content-Type")).matches("application/json.*"); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).field("['status']").isEqualTo("OK"); } generated tests
  19. @Test public void validate_validSizeShouldReturnHttpOk() throws Exception { // given: MockMvcRequestSpecification

    request = given() .header("Content-Type", "application/json") .body("{\"age\":\"20\"}"); // when: ResponseOptions response = given().spec(request) .post("/check"); // then: assertThat(response.statusCode()).isEqualTo(200); assertThat(response.header("Content-Type")).matches("application/json.*"); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).field("['status']").isEqualTo("OK"); } generated tests
  20. @Test public void validate_validSizeShouldReturnHttpOk() throws Exception { // given: MockMvcRequestSpecification

    request = given() .header("Content-Type", "application/json") .body("{\"age\":\"20\"}"); // when: ResponseOptions response = given().spec(request) .post("/check"); // then: assertThat(response.statusCode()).isEqualTo(200); assertThat(response.header("Content-Type")).matches("application/json.*"); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).field("['status']").isEqualTo("OK"); } generated testS
  21. { "request" : { "url" : "/check", "method" : "POST",

    "headers" : { "Content-Type" : { "matches" : "application/json.*" } }, "bodyPatterns" : [ { "matchesJsonPath" : "$[?(@.['age'] =~ /[2-9][0-9]/)]" } ] }, "response" : { "status" : 200, "body" : "{\"status\":\"OK\"}", "headers" : { "Content-Type" : "application/json" } } } generated stub
  22. { "request" : { "url" : "/check", "method" : "POST",

    "headers" : { "Content-Type" : { "matches" : "application/json.*" } }, "bodyPatterns" : [ { "matchesJsonPath" : "$[?(@.['age'] =~ /[2-9][0-9]/)]" } ] }, "response" : { "status" : 200, "body" : "{\"status\":\"OK\"}", "headers" : { "Content-Type" : "application/json" } } } generated stub
  23. CONSUMER TESTING WITH STUBS @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = WebEnvironment.MOCK) @AutoConfigureStubRunner( workOffline

    = true, ids = "com.example:beer-api-producer-external:+:stubs:8090" ) public class BeerControllerTest extends AbstractTest { @Test void should_give_me_a_beer_when_im_old_enough() { mockMvc.perform(MockMvcRequestBuilders.post("/beer") .contentType(MediaType.APPLICATION_JSON) .content(json.write(new Person("John Doe", 20)).getJson())) .andExpect(status().isOk()) .andExpect(content().string("THERE YOU GO")); } }
  24. CONSUMER TESTING WITH STUBS @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = WebEnvironment.MOCK) @AutoConfigureStubRunner( workOffline

    = true, ids = "com.example:beer-api-producer-external:+:stubs:8090" ) public class BeerControllerTest extends AbstractTest { @Test void should_give_me_a_beer_when_im_old_enough() { mockMvc.perform(MockMvcRequestBuilders.post("/beer") .contentType(MediaType.APPLICATION_JSON) .content(json.write(new Person("John Doe", 20)).getJson())) .andExpect(status().isOk()) .andExpect(content().string("THERE YOU GO")); } }