Slide 1

Slide 1 text

Olga Maciaszek-Sharma June 2020 Better APIs, faster tests and more resilient systems with Spring Cloud Contract Copyright © 2020 VMware, Inc. or its affiliates.

Slide 2

Slide 2 text

About me Software Engineer in Spring Cloud Team ● Spring Cloud LoadBalancer (Spring Cloud Commons) ● Spring Cloud Contract ● Spring Cloud OpenFeign ● Spring Cloud Netflix https://github.com/OlgaMaciaszek https://twitter.com/olga_maciaszek

Slide 3

Slide 3 text

What will we try to improve?

Slide 4

Slide 4 text

Faster Tests

Slide 5

Slide 5 text

The Monolith Service 1 Service 2 Service 3

Slide 6

Slide 6 text

A LOT of E2E tests

Slide 7

Slide 7 text

Failing pipelines source: https://www.jenkins.io/blog/2016/05/26/introducing-blue-ocean/

Slide 8

Slide 8 text

Long build and test times source: https://ci.jenkins.io/

Slide 9

Slide 9 text

Hell automation @venkat_s

Slide 10

Slide 10 text

E2E tests Slow Unstable Rely on collaborators Reliable

Slide 11

Slide 11 text

Distributed architectures IoT API Gateway Mobile Browser Service 1 Service 3 Config Server Service Registry Spring Cloud Sleuth Service 2 Metrics Store Databases Message Brokers PLATFORM

Slide 12

Slide 12 text

Fast development, fast tests

Slide 13

Slide 13 text

Decoupling deployment

Slide 14

Slide 14 text

Agile teams, agile releases

Slide 15

Slide 15 text

Fail Safe Safe To Fail

Slide 16

Slide 16 text

Shift to the Left

Slide 17

Slide 17 text

Network Communication Stubs

Slide 18

Slide 18 text

A new endpoint { “request” : { “method”: “GET”, “url” : “/endpointX”, }, “response” : { “status” : 200 } }

Slide 19

Slide 19 text

A new endpoint { “request” : { “method”: “GET”, “url” : “/endpointX”, }, “response” : { “status” : 200 } }

Slide 20

Slide 20 text

A new endpoint

Slide 21

Slide 21 text

A new endpoint

Slide 22

Slide 22 text

A new endpoint { “request” : { “method”: “GET”, “url” : “/endpointX”, }, “response” : { “status” : 200 } } { “request” : { “method”: “GET”, “url” : “/endpointY”, }, “response” : { “status” : 200 } }

Slide 23

Slide 23 text

Messaging topic: yyy class Product { int productId; String productName; } topic: xxx class Product { int id; String productName; } topic: xxx class Product { int productId; String productName; }

Slide 24

Slide 24 text

Stubs alone don’t guarantee ANYTHING

Slide 25

Slide 25 text

Stubbed tests Unreliable Fast Stable Independent

Slide 26

Slide 26 text

Better APIs

Slide 27

Slide 27 text

The Monolith Service 1 Service 2 Service 3

Slide 28

Slide 28 text

A server with limited clients

Slide 29

Slide 29 text

Limited APIs

Slide 30

Slide 30 text

External APIs

Slide 31

Slide 31 text

Limited Network Communication for Internal Apps

Slide 32

Slide 32 text

Distributed architectures IoT API Gateway Mobile Browser Service 1 Service 3 Config Server Service Registry Spring Cloud Sleuth Service 2 Metrics Store Databases Message Brokers PLATFORM

Slide 33

Slide 33 text

Network communication as first-class citizen

Slide 34

Slide 34 text

Multiple instances of many services communicating with each other over the network

Slide 35

Slide 35 text

Numerous fast-growing and (even) changing internal APIs

Slide 36

Slide 36 text

Possibility of COLLABORATION between API producers and consumers

Slide 37

Slide 37 text

Some terms Producer Consumer

Slide 38

Slide 38 text

Why would we want to do that?

Slide 39

Slide 39 text

It’s the consumer that actually CONSUMES the API.

Slide 40

Slide 40 text

It makes sense for the consumer to actually DRIVE the creation of the APIs

Slide 41

Slide 41 text

No content

Slide 42

Slide 42 text

Potential Answer: Consumer-Driven Contracts

Slide 43

Slide 43 text

Contracts Contract Consumer Input A Producer Output B

Slide 44

Slide 44 text

Consumer-Driven Contracts https://martinfowler.com/articles/consumerDrive nContracts.html https://martinfowler.com/bliki/ContractTest.html

Slide 45

Slide 45 text

Possible Implementation: Spring Cloud Contract

Slide 46

Slide 46 text

Spring Cloud Contract - Consumer-Driven Contract Flow Consumer ● Checks out Producer code repo and works locally on a contract ● Creates a pull request with the new Contract ● Collaborates with the Producer on the Contract PR ● Uses stubs published by the producer along the app for integration tests Producer ● Reviews the Contract PR ● Runs build on the PR branch (FAILURE) ● Adds required endpoints ● Runs build on the PR branch (PASSING) ● Code Deployed along with generated stubs jar

Slide 47

Slide 47 text

Producer Build Setup org.springframework.cloud spring-cloud-contract-maven-plugin 2.2.3.RELEASE true JUNIT5 WEBTESTCLIENT FraudCheckTestsBaseClass org.springframework.boot spring-boot-maven-plugin

Slide 48

Slide 48 text

Contract Contract.make { request { method 'POST' url '/fraudcheck' body( clientId: anyUuid(), loanAmount: 99999 ) headers { header('Content-Type', 'application/vnd.fraud.v1+json;charset=UTF-8')} } response { status 200 body( fraudCheckStatus: "FRAUD", rejectionReason: "Amount too high") headers { header('Content-Type': 'application/vnd.fraud.v1+json;charset=UTF-8')} } }

Slide 49

Slide 49 text

Contract request: method: POST url: /fraudcheck body: clientId: 437ea752-7361-4d55-ad3d-2dcc40204acf loanAmount: 99999 headers: Content-Type: application/vnd.fraud.v1+json;charset=UTF-8 matchers: body: - path: $.['clientId'] type: by_regex predefined: uuid response: status: 200 body: fraudCheckStatus: "FRAUD" rejectionReason: "Amount too high" headers: Content-Type: application/vnd.fraud.v1+json;charset=UTF-8

Slide 50

Slide 50 text

Spring Cloud Contract - Consumer-Driven Contract Flow Consumer ● Checks out Producer code repo and works locally on a contract ● Creates a pull request with the new Contract ● Collaborates with the Producer on the Contract PR ● Uses stubs published by the producer along the app for integration tests Producer ● Reviews the Contract PR ● Runs build on the PR branch (FAILURE) ● Adds required endpoints ● Runs build on the PR branch (PASSING) ● Code Deployed along with generated stubs jar

Slide 51

Slide 51 text

Spring Cloud Contract - Consumer-Driven Contract Flow Consumer ● Checks out Producer code repo and works locally on a contract ● Creates a pull request with the new Contract ● Collaborates with the Producer on the Contract PR ● Uses stubs published by the producer along the app for integration tests Producer ● Reviews the Contract PR ● Runs build on the PR branch (FAILURE) ● Adds required endpoints ● Runs build on the PR branch (PASSING) ● Code Deployed along with generated stubs jar

Slide 52

Slide 52 text

Spring Cloud Contract - Consumer-Driven Contract Flow Consumer ● Checks out Producer code repo and works locally on a contract ● Creates a pull request with the new Contract ● Collaborates with the Producer on the Contract PR ● Uses stubs published by the producer along the app for integration tests Producer ● Reviews the Contract PR ● Runs build on the PR branch (FAILURE) ● Adds required endpoints ● Runs build on the PR branch (PASSING) ● Code Deployed along with generated stubs jar

Slide 53

Slide 53 text

Spring Cloud Contract - Consumer-Driven Contract Flow Consumer ● Checks out Producer code repo and works locally on a contract ● Creates a pull request with the new Contract ● Collaborates with the Producer on the Contract PR ● Uses stubs published by the producer along the app for integration tests Producer ● Reviews the Contract PR ● Runs build on the PR branch (FAILURE) ● Adds required endpoints ● Runs build on the PR branch (PASSING) ● Code Deployed along with generated stubs jar

Slide 54

Slide 54 text

Generated Contract Test @Test public void validate_shouldMarkClientAsFraud() throws Exception { // given: WebTestClientRequestSpecification request = given() .header("Content-Type", application/vnd.fraud.v1+json;charset=UTF-8") .body("{\"clientId\":\"437ea752-7361-4d55-ad3d-2dcc40204acf\",\"loanAmount\":99999}"); // when: WebTestClientResponse response = given().spec(request) .post("/fraudcheck"); // then: assertThat(response.statusCode()).isEqualTo(200); assertThat(response.header("Content-Type")) .isEqualTo("application/vnd.fraud.v1+json;charset=UTF-8"); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).field("['fraudCheckStatus']").isEqualTo("FRAUD"); assertThatJson(parsedJson).field("['rejectionReason']").isEqualTo("Amount too high"); }

Slide 55

Slide 55 text

Spring Cloud Contract - Consumer-Driven Contract Flow Consumer ● Checks out Producer code repo and works locally on a contract ● Creates a pull request with the new Contract ● Collaborates with the Producer on the Contract PR ● Uses stubs published by the producer along the app for integration tests Producer ● Reviews the Contract PR ● Runs build on the PR branch (FAILURE) ● Adds required endpoints ● Runs build on the PR branch (PASSING) ● Code Deployed along with generated stubs jar

Slide 56

Slide 56 text

Spring Cloud Contract - Consumer-Driven Contract Flow Consumer ● Checks out Producer code repo and works locally on a contract ● Creates a pull request with the new Contract ● Collaborates with the Producer on the Contract PR ● Uses stubs published by the producer along the app for integration tests Producer ● Reviews the Contract PR ● Runs build on the PR branch (FAILURE) ● Adds required endpoints ● Runs build on the PR branch (PASSING) ● Code Deployed along with generated stubs jar

Slide 57

Slide 57 text

Generated WireMock Stub { "id" : "88dd475b-005d-4a1d-a6c5-3d40493c8d37", "request" : { "url" : "/fraudcheck", "method" : "POST", "headers" : { "Content-Type" : { "equalTo" : "application/vnd.fraud.v1+json;charset=UTF-8" }}, "bodyPatterns" : [ { "matchesJsonPath" : "$[?(@.['loanAmount'] == 99999)]" }, { "matchesJsonPath" : "$[?(@.['clientId'] =~ /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/)]" } ] }, "response" : { "status" : 200, "body" : "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}", "headers" : { "Content-Type" : "application/vnd.fraud.v1+json;charset=UTF-8" }, "transformers" : [ "response-template" ]}, "uuid" : "88dd475b-005d-4a1d-a6c5-3d40493c8d37"}

Slide 58

Slide 58 text

Spring Cloud Contract - Consumer-Driven Contract Flow Consumer ● Checks out Producer code repo and works locally on a contract ● Creates a pull request with the new Contract ● Collaborates with the Producer on the Contract PR ● Uses stubs published by the producer along the app for integration tests Producer ● Reviews the Contract PR ● Runs build on the PR branch (FAILURE) ● Adds required endpoints ● Runs build on the PR branch (PASSING) ● Code Deployed along with generated stubs jar

Slide 59

Slide 59 text

Consumer Build Setup org.springframework.cloud spring-cloud-starter-contract-stub-runner test

Slide 60

Slide 60 text

Consumer Tests @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @ExtendWith({SpringExtension.class}) @AutoConfigureStubRunner(ids = {"io.github.olgamaciaszek:scc-producer-demo:+:stubs:8080"}, stubsMode = StubRunnerProperties.StubsMode.REMOTE) class SccConsumerDemoApplicationTests { @Test void shouldBeRejectedDueToAbnormalLoanAmount() { LoanApplication application = new LoanApplication(new Client(UUID.randomUUID()), new BigDecimal("99999"), UUID.randomUUID()); LoanApplicationResult loanApplication = loanApplicationService .loanApplication(application).block(); assertThat(loanApplication.getLoanApplicationStatus()) .isEqualTo(LoanApplicationStatus.APPLICATION_REJECTED); assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high"); }}

Slide 61

Slide 61 text

Consumer Tests @StubRunnerPort port; stubrunner: ids: - org.springframework.cloud.contract.verifier.stubs:loanIssuance - org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer - org.springframework.cloud.contract.verifier.stubs:bootService

Slide 62

Slide 62 text

Consumer Tests @Rule public StubRunnerRule rule = new StubRunnerRule() .downloadStub("com.example","fraud-service") .stubsMode(StubRunnerProperties.StubsMode.LOCAL) .withStubPerConsumer(true) .withConsumerName("loan-service"); int fraudServicePort = rule.findStubUrl("fraud-service").getPort()

Slide 63

Slide 63 text

Code Demo

Slide 64

Slide 64 text

Messaging Contract Contract.make { label "payment_successful" input { triggeredBy "makeSuccessfulPayment()" } outputMessage { sentTo "payments" body( uuid: "d64c361b-29bb-43a9-8fe8-5e7e05493842", status: "SUCCESS" ) headers { header("contentType", applicationJson())}}}

Slide 65

Slide 65 text

Generated Contract Test @Test public void validate_should_payment_fail() throws Exception { // when: makeFailingPayment(); // then: ContractVerifierMessage response = contractVerifierMessaging.receive("payments"); assertThat(response).isNotNull(); // and: assertThat(response.getHeader("contentType")).isNotNull(); assertThat(response.getHeader("contentType").toString()).isEqualTo("application/json"); // and: DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload())); assertThatJson(parsedJson).field("['uuid']").isEqualTo("d64c361b-29bb-43a9-8fe8-5e7e05493842"); assertThatJson(parsedJson).field("['status']").isEqualTo("FAILURE"); }

Slide 66

Slide 66 text

Consumer Test @SpringBootTest @AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL, ids = "com.example:scc-payment") class PaymentProcessingSpec extends Specification { @Autowired StubTrigger stubTrigger def 'should handle successful payment'() { when: stubTrigger.trigger('payment_successful') then: accountingService.successfulPayments.size() == 1 }

Slide 67

Slide 67 text

Code Demo

Slide 68

Slide 68 text

Spring Cloud Contract A few highlighted features out of many: ● Contracts in a separate repository ● Spring Cloud Netflix and Spring Cloud LoadBalancer integration ● Creating Spring Rest Docs from Contracts and Contracts from Spring Rest Docs ● Polyglot support (using provided Docker image) ● Community support for Swagger files

Slide 69

Slide 69 text

Architecture and Deployments

Slide 70

Slide 70 text

Architecture/ API TDD Refactor Red Green

Slide 71

Slide 71 text

Backward Compatibility Checks in Pipelines New API version Test with PROD contracts Deploy Test with new contracts

Slide 72

Slide 72 text

Deferred updates Deployment Contract fields Application fields 0 { "buildTime": 5 } { "buildTime": 5 } 1 { "buildTime": 5, "buildTimeMillis": 5000 } { "buildTime": 5, "buildTimeMillis": 5000 } 2 { "buildTimeMillis": 5000 } { "buildTime": 5, "buildTimeMillis": 5000 } 3 { "buildTimeMillis": 5000 } { "buildTimeMillis": 5000 }

Slide 73

Slide 73 text

Lessons Learnt and Good Practices

Slide 74

Slide 74 text

Usage Logic tests in lower layers

Slide 75

Slide 75 text

Resilience Monitoring Logging Alerting

Slide 76

Slide 76 text

Maintenance Skipping contracts for non-essential functionalities

Slide 77

Slide 77 text

Maintenance Skipping multiple values

Slide 78

Slide 78 text

Resources ● Project page ● Samples ● https://github.com/OlgaMaciaszek/scc-producer-demo ● https://github.com/OlgaMaciaszek/scc-consumer-demo ● https://github.com/OlgaMaciaszek/scc-payment ● https://github.com/OlgaMaciaszek/scc-shop ● Spring Cloud Contract at Devskiller - case study ● https://gitter.im/spring-cloud/spring-cloud-contract

Slide 79

Slide 79 text

Thank you Contact me at omaciaszeksh@vmware.com © 2020 Spring. A VMware-backed project.