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

Better APIs, faster tests and more resilient systems with Spring Cloud Contract

Better APIs, faster tests and more resilient systems with Spring Cloud Contract

How can we better collaborate on creating useful APIs? How to reduce the testing time of your application? How can the team build API backwards compatibility verification into their deployment process?

In this talk, I will discuss how to leverage the Consumer-Driven Contracts approach to solve these and other issues and present one of CDC implementations - Spring Cloud Contract. We will discuss practical use-cases and look at code examples. I will also point out some of the possible issues and ways of handling them, as well as some good practices I have learnt while working both as a CDC tools end-user and their developer.

Olga Maciaszek-Sharma

June 30, 2020
Tweet

More Decks by Olga Maciaszek-Sharma

Other Decks in Technology

Transcript

  1. Olga Maciaszek-Sharma June 2020 Better APIs, faster tests and more

    resilient systems with Spring Cloud Contract Copyright © 2020 VMware, Inc. or its affiliates.
  2. 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
  3. 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
  4. A new endpoint { “request” : { “method”: “GET”, “url”

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

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

    : “/endpointX”, }, “response” : { “status” : 200 } } { “request” : { “method”: “GET”, “url” : “/endpointY”, }, “response” : { “status” : 200 } }
  7. 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; }
  8. 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
  9. 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
  10. Producer Build Setup <build> <plugins> <plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>2.2.3.RELEASE</version> <extensions>true</extensions>

    <configuration> <testFramework>JUNIT5</testFramework> <testMode>WEBTESTCLIENT</testMode> <baseClassForTests>FraudCheckTestsBaseClass</baseClassForTests> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
  11. 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')} } }
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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"); }
  18. 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
  19. 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
  20. 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"}
  21. 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
  22. 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"); }}
  23. 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()
  24. 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())}}}
  25. 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"); }
  26. 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 }
  27. 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
  28. Backward Compatibility Checks in Pipelines New API version Test with

    PROD contracts Deploy Test with new contracts
  29. 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 }
  30. 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