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

Build resilient systems with Spring Cloud Contract and Testcontainers

Build resilient systems with Spring Cloud Contract and Testcontainers

Verifying behaviors of the cloud-native applications and ensuring that all of the services in the system work correctly together is both crucial and challenging. Manually maintaining environments to test the correctness of the entire system is undevops-like and fragile.

Luckily, modern tools can help you to build automated, reliable test pipelines, and in this session, we explore how using Spring Cloud Contract and Testcontainers together can improve your testing and deployment processes.

Spring Cloud Contract is an implementation of Consumer-Driven Contracts, an approach that provides a way to easily describe and verify APIs, at the same time allowing building API backward compatibility verification into the deployment process.

Testcontainers lets developers programmatically build test environments consisting of real services running in lightweight and disposable containers. It turns the process of integration testing into a seamless, unit-test-like experience.

In this presentation, we’ll show how contract and integration tests complement each other and explore one of the most natural and reliable approaches to service evolution with contract testing. We’ll discuss why in Spring Cloud Contract, we’ve decided to switch to using Testcontainers as the solution for Kafka and AMQP messaging verification and demonstrate practical use-cases and code examples of how to set up both types of tests in your applications and deployment pipelines.

Olga Maciaszek-Sharma

May 19, 2023
Tweet

More Decks by Olga Maciaszek-Sharma

Other Decks in Technology

Transcript

  1. Olga Maciaszek-Sharma Oleg Šelajev May 2023 Build resilient systems with

    Spring Cloud Contract and Testcontainers Copyright © 2023 VMware, Inc. or its affiliates.
  2. About Olga Senior Software Engineer in Spring Cloud Team •

    Spring Cloud LoadBalancer (Spring Cloud Commons) • Spring Interface Clients • Spring Cloud Contract • Spring Cloud OpenFeign • Spring Cloud Netflix • Spring Native support for Spring Cloud projects • RSocket HTTP Bridge https://github.com/OlgaMaciaszek https://twitter.com/olga_maciaszek
  3. A new message topic: servings body: { "beverages": [ {

    "uuid": "16b439a4-6ace-4901-8fb6-728588c12da8", "coffee": { "name": "Latte", "coffeeContent": 60, "steamedMilkContent": 180, "milkFoamContent": 5 } } // … ] } headers: “contentType”: “application/json” “testKey1”: “testValue1”
  4. A new message topic: servings body: { "beverages": [ {

    "uuid": "16b439a4-6ace-4901-8fb6-728588c12da8", "coffee": { "name": "Latte", "coffeeContent": 60, "steamedMilkContent": 180, "milkFoamContent": 5 } } // … ] } headers: “contentType”: “application/json” “testKey1”: “testValue1”
  5. A new message topic: serving body: { "beverages": [ {

    "uuid": "16b439a4-6ace-4901-8fb6-728588c12da8", "coffee": { "name": "Latte", "coffeeContent": 60, "steamedMilkContent": 180, "milkFoamContent": 5 } } // … ] } headers: “contentType”: “application/json” “testKey1”: “testValue1”
  6. A new message topic: servings body: { "beverages": [ {

    "uuid": "16b439a4-6ace-4901-8fb6-728588c12da8", "coffee": { "name": "Latte", "coffeeContent": 60, "steamedMilkContent": 180, "milkFoamContent": 5 } } // … ] } headers: “contentType”: “application/json” “testKey1”: “testValue1”
  7. A new message topic: servings body: { "beverages": [ {

    "id": "16b439a4-6ace-4901-8fb6-728588c12da8", "coffee": { "name": "Latte", "coffeeContent": 60, "steamedMilkContent": 180, "milkFoamContent": 5 } } // … ] } headers: “contentType”: “application/json” “testKey1”: “testValue1”
  8. A new message topic: servings body: { "beverages": [ {

    "uuid": "16b439a4-6ace-4901-8fb6-728588c12da8", "coffee": { "name": "Latte", "coffeeContent": 60, "steamedMilkContent": 180, "milkFoamContent": 5 } } // … ] } headers: “contentType”: “application/json” “testKey1”: “testValue1”
  9. A new message topic: servings body: { "beverages": [ {

    "uuid": "16b439a4-6ace-4901-8fb6-728588c12da8", "coffee": { "name": "Latte", "coffeeContent": 60, "steamedMilkContent": 180, "milkFoamContent": 5 } } // … ] } headers: “contentType”: “application/json” “testKey1”: “testValue1”
  10. Producer Build Setup <build> <plugins> <plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>4.0.3-SNAPSHOT</version> <extensions>true</extensions>

    <configuration> <testFramework>JUNIT5</testFramework> <baseClassForTests>io.xxx.BaseTestClass</baseClassForTests> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
  11. • Setup the environment • Create inputs • Produce the

    outputs (with production code) Producer test setup
  12. Contract Contract.make { / ... outputMessage { sentTo("servings") body([beverages: [[uuid

    : $(anyUuid()), coffee: [ name : "Latte", coffeeContent : "60", steamedMilkContent: "180", milkFoamContent : "5" ]]]]) / ... }
  13. Generated Contract Test @Test public void validate_shouldSendServing() throws Exception {

    // when: triggerServing(); // then: ContractVerifierMessage response = contractVerifierMessaging.receive("servings", contract(this, "shouldSendServing.yml")); assertThat(response).isNotNull(); // and: assertThat(response.getHeader("contentType")).isNotNull(); assertThat(response.getHeader("contentType").toString()).isEqualTo("application/json"); assertThat(response.getHeader("testKey1")).isNotNull(); assertThat(response.getHeader("testKey1").toString()).isEqualTo("testValue1"); // ...
  14. Generated Contract Test @Test public void validate_shouldSendServing() throws Exception {

    // ... DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload())); assertThatJson(parsedJson).array("['beverages']").contains("['uuid']") .matches("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}") assertThatJson(parsedJson).array("['beverages']").field("['coffee']").field("['name']") .isEqualTo("Latte"); assertThatJson(parsedJson).array("['beverages']").field("['coffee']").field("['coffeeContent'] ") .isEqualTo("60"); assertThatJson(parsedJson).array("['beverages']").field("['coffee']").field("['steamedMilkCont ent']") .isEqualTo("180"); assertThatJson(parsedJson).array("['beverages']").field("['coffee']").field("['milkFoamContent ']") .isEqualTo("5");
  15. Teamwork - different approaches Provider Contracts • Contracts with the

    producer • The producer has no knowledge of how consumers use their API • The producer can impose contract changes on consumers Consumer Contracts • Contracts with the consumers • Possible issues with conflict resolution in case of multiple consumers • In some implementations, the consumers can impose contract changes on the producer Consumer-Driven Contracts • Contracts with the producer ◦ If security issues, an external repo • Consumers as key participants in the creation of the contracts • Convenient handling of multiple consumers
  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. 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
  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. 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
  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. 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
  23. 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
  24. Backward Compatibility Checks in Pipelines New API version Test with

    PROD contracts Deploy Test with new contracts
  25. Producer Build Setup - Compatibility Tests with Profiles <profile> <id>apicompatibility</id>

    <build> <plugins> <plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>4.0.3-SNAPSHOT</version> <configuration> <contractsRepositoryUrl>${repo.with.binaries}</contractsRepositoryUrl> <contractsMode>REMOTE</contractsMode> <contractDependency> <groupId>${project.groupId}</groupId> <artifactId>${project.artifactId}</artifactId> <classifier>stubs</classifier> <version>${latest.production.version}</version> </contractDependency> </configuration> </plugin> </plugins> </build> </profile>
  26. Evolving the system safely Deployment Contract fields Producer fields 0

    { "uuid": "xxx" } { "uuid": "xxx" } 1 { "uuid": "xxx", "id": 5 } { "uuid": “xxx”, "id": 5 } 2 { "id": 5 } { "uuid": “xxx”, "id": 5 } 3 { "id": 5 } { "id": 5 }