Slide 1

Slide 1 text

CONSUMER DRIVEN CONTRACTS Boost your confidence with @ordepdev

Slide 2

Slide 2 text

Testing is hard The pyramid is, most of the times, inverted.

Slide 3

Slide 3 text

BUILDING MICROSERVICES WITHOUT BREAKING CONTRACTS IS EVEN HARDER

Slide 4

Slide 4 text

MONOLITHS VS MICROSERVICES MONOLITH build test deploy

Slide 5

Slide 5 text

MONOLITHS VS MICROSERVICES build test deploy build test deploy build test deploy microservices a b c

Slide 6

Slide 6 text

HOW DO WE TEST MICROSERVICES?

Slide 7

Slide 7 text

integration Mocking, Faking, Stubbing, you choose.

Slide 8

Slide 8 text

INTEGRATION TESTS build test deploy We build our own stubs in order to make our tests pass a b

Slide 9

Slide 9 text

end-to-end Those bloated and long to run… The feedback from this slide is already late.

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

# 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

Slide 17

Slide 17 text

@Test public void check_withValidAge_shouldReturnHttpStatusOk() throws Exception { ResponseEntity 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

Slide 18

Slide 18 text

@Test public void check_withValidAge_shouldReturnHttpStatusOk() throws Exception { ResponseEntity 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

Slide 19

Slide 19 text

@Test public void check_withValidAge_shouldReturnHttpStatusOk() throws Exception { ResponseEntity 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

Slide 20

Slide 20 text

@Test public void check_withValidAge_shouldReturnHttpStatusOk() throws Exception { ResponseEntity 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

Slide 21

Slide 21 text

@Test public void check_withValidAge_shouldReturnHttpStatusOk() throws Exception { ResponseEntity 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

Slide 22

Slide 22 text

@Test public void check_withValidAge_shouldReturnHttpStatusOk() throws Exception { ResponseEntity 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

Slide 23

Slide 23 text

after deploy…

Slide 24

Slide 24 text

“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?

Slide 25

Slide 25 text

“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?

Slide 26

Slide 26 text

Expectations Contract Testing is all about… Shared between the producer and the consumer.

Slide 27

Slide 27 text

SPRING CLOUD CONTRACT

Slide 28

Slide 28 text

PRODUCER CONSUMER Common repository with contracts

Slide 29

Slide 29 text

PRODUCER CONSUMER Creates a PR with the proposed contracts

Slide 30

Slide 30 text

PRODUCER CONSUMER Review contracts and writes the missing implementation

Slide 31

Slide 31 text

PRODUCER CONSUMER Merge PR with the proposed and validated contracts

Slide 32

Slide 32 text

PRODUCER CONSUMER Contracts JAR gets deployed into Nexus CONSUMER

Slide 33

Slide 33 text

STUBS ALWAYS MATCH SERVER-SIDE IMPLEMENTATION

Slide 34

Slide 34 text

CHANGES ARE IMMEDIATELY VISIBLE ON BOTH SIDES

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

@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

Slide 38

Slide 38 text

@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

Slide 39

Slide 39 text

@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

Slide 40

Slide 40 text

{ "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

Slide 41

Slide 41 text

{ "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

Slide 42

Slide 42 text

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")); } }

Slide 43

Slide 43 text

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")); } }

Slide 44

Slide 44 text

A contract needs to speak the truth.

Slide 45

Slide 45 text

How can we verify the truth?

Slide 46

Slide 46 text

Running a test!

Slide 47

Slide 47 text

CONFIDENCE Continuously evolve your codebase without breaking contracts

Slide 48

Slide 48 text

Faster No need to setup end-to-end environments

Slide 49

Slide 49 text

Less Error Prone Verification of Contracts are automatically managed

Slide 50

Slide 50 text

Thank you. @ordepdev

Slide 51

Slide 51 text

CONSUMER DRIVEN CONTRACTS Boost your confidence with @ordepdev