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.

    View Slide

  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

    View Slide

  3. What will we try to improve?

    View Slide

  4. Faster Tests

    View Slide

  5. The Monolith
    Service 1 Service 2 Service 3

    View Slide

  6. A LOT of E2E tests

    View Slide

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

    View Slide

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

    View Slide

  9. Hell automation
    @venkat_s

    View Slide

  10. E2E tests
    Slow
    Unstable
    Rely on collaborators
    Reliable

    View Slide

  11. 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

    View Slide

  12. Fast development, fast tests

    View Slide

  13. Decoupling deployment

    View Slide

  14. Agile teams, agile releases

    View Slide

  15. Fail Safe Safe To Fail

    View Slide

  16. Shift to the Left

    View Slide

  17. Network Communication
    Stubs

    View Slide

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

    View Slide

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

    View Slide

  20. A new endpoint

    View Slide

  21. A new endpoint

    View Slide

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

    View Slide

  23. 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;
    }

    View Slide

  24. Stubs alone don’t guarantee
    ANYTHING

    View Slide

  25. Stubbed tests
    Unreliable
    Fast
    Stable
    Independent

    View Slide

  26. Better APIs

    View Slide

  27. The Monolith
    Service 1 Service 2 Service 3

    View Slide

  28. A server with limited clients

    View Slide

  29. Limited APIs

    View Slide

  30. External APIs

    View Slide

  31. Limited Network
    Communication for Internal
    Apps

    View Slide

  32. 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

    View Slide

  33. Network communication as
    first-class citizen

    View Slide

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

    View Slide

  35. Numerous fast-growing and
    (even) changing internal APIs

    View Slide

  36. Possibility of
    COLLABORATION between
    API producers and
    consumers

    View Slide

  37. Some terms
    Producer Consumer

    View Slide

  38. Why would we want to do
    that?

    View Slide

  39. It’s the consumer that
    actually CONSUMES the API.

    View Slide

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

    View Slide

  41. View Slide

  42. Potential Answer:
    Consumer-Driven Contracts

    View Slide

  43. Contracts
    Contract
    Consumer
    Input A
    Producer
    Output B

    View Slide

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

    View Slide

  45. Possible Implementation:
    Spring Cloud Contract

    View Slide

  46. 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

    View Slide

  47. 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



    View Slide

  48. 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')}
    }
    }

    View Slide

  49. 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

    View Slide

  50. 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

    View Slide

  51. 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

    View Slide

  52. 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

    View Slide

  53. 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

    View Slide

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

    View Slide

  55. 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

    View Slide

  56. 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

    View Slide

  57. 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"}

    View Slide

  58. 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

    View Slide

  59. Consumer Build Setup

    org.springframework.cloud
    spring-cloud-starter-contract-stub-runner
    test

    View Slide

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

    View Slide

  61. 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

    View Slide

  62. 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()

    View Slide

  63. Code Demo

    View Slide

  64. 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())}}}

    View Slide

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

    View Slide

  66. 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
    }

    View Slide

  67. Code Demo

    View Slide

  68. 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

    View Slide

  69. Architecture and Deployments

    View Slide

  70. Architecture/ API TDD
    Refactor
    Red
    Green

    View Slide

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

    View Slide

  72. 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
    }

    View Slide

  73. Lessons Learnt and Good
    Practices

    View Slide

  74. Usage
    Logic tests in lower layers

    View Slide

  75. Resilience
    Monitoring
    Logging
    Alerting

    View Slide

  76. Maintenance
    Skipping contracts for non-essential
    functionalities

    View Slide

  77. Maintenance
    Skipping multiple values

    View Slide

  78. 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

    View Slide

  79. Thank you
    Contact me at [email protected]
    © 2020 Spring. A VMware-backed project.

    View Slide