Skip to content

Instantly share code, notes, and snippets.

@marcingrzejszczak
Last active April 26, 2024 08:30
Show Gist options
  • Save marcingrzejszczak/2033d0a1a52d6636d7fa8c40ea4363bd to your computer and use it in GitHub Desktop.
Save marcingrzejszczak/2033d0a1a52d6636d7fa8c40ea4363bd to your computer and use it in GitHub Desktop.
Spring Cloud Contract Polyglot Support

Spring Cloud Contract Polyglot Support

Table of contents

  • What is Contract Testing

    • different types of tests - all about fast feedback

    • end to end testing as a potential solution to integration testing

    • brittlness and slowness as a source of frustration

    • contract testing as a potential solution to the problem

  • Introduction to the maven nomenclature

  • What is Spring Cloud Contract

  • What is the current "problem" with Spring Cloud Contract

    • Groovy DSL

    • you need to have Java installed

    • can’t generate tests for the producer in a non JVM world

  • How the problem is solved with Spring Cloud Contract 1.2.3.RELEASE / Edgware.SR2

    • producer

    • consumer

This article will contain a short reminder of what Contract Testing, how Spring Cloud Contract solves it and how Spring Cloud Contract can be used in a polyglot world.

What is Contract Testing

In order to increase the certainty that our system behaves properly, we write different types of tests. According to the test pyramid the main types of tests are unit, integration and UI tests. The more complex the tests are the more time and effort their require and introduce more brittleness.

In a distributed system, one of the most frequent problems is testing integrations between applications. Let’s assume that your service is sending a REST request to another application. When using Spring Boot, you can write a @SpringBootTest where you will test that behaviour. You set up a Spring context, you prepare a request to be sent…​ and where do you send it? You haven’t started the other application, so you will get a Connection Refused exception. What you can do is either mock the real HTTP call and return a fake response. However, if you do that, you will not test any real HTTP integration, serialization and deserialization mechanisms etc. Instead you can start a fake HTTP server (for example WireMock) and simulate how it should behave. The problem faced here is that you as a client of an API define how the server behaves. In other words, if you tell the fake server to return text bar when a request is sent to endpoint /foo, then it will just do it. Even if the real server doesn’t have such an endpoint. So in this case we’re talking about the problem related to reliability of stubs.

It’s always tempting to set up an environment for end to end tests, where we will spawn all applications and perform tests running though the whole system. Often, that’s a good solution that will increase the confidence that your business features are still working fine. Often, the problem with end to end tests is that they fail for no apparent reason and are very slow. There is nothing more frustrating then seeing that, after running for 10 hours, the end to end tests have failed due to a typo in the API call.

A potential solution to this problem are Contract Tests. Before, we go into details of what those are, let’s define some terms:

  • producer - is the owner of the e.g. HTTP API, or a producer of a message sent via e.g. RabbitMQ

  • consumer - is the application that consumes the e.g. HTTP API, or listens to a message received via e.g. RabbitMQ

  • contract - is an agreement between the producer and the consumer on how the communication should look like. It’s not a schema. It’s more of a scenario of usage. E.g. for this particular scenario, I need such input and then I’ll reply with such output.

Example of a contract written in YAML:

request: # (1)
  method: PUT # (2)
  url: /fraudcheck # (3)
  body: # (4)
    "client.id": 1234567890
    loanAmount: 99999
  headers: # (5)
    Content-Type: application/json
  matchers:
    body:
      - path: $.['client.id'] # (6)
        type: by_regex
        value: "[0-9]{10}"
response: # (7)
  status: 200 # (8)
  body:  # (9)
    fraudCheckStatus: "FRAUD"
    "rejection.reason": "Amount too high"
  headers: # (10)
    Content-Type: application/json;charset=UTF-8


#From the Consumer perspective, when shooting a request in the integration test:
#
#(1) - If the consumer sends a request
#(2) - With the "PUT" method
#(3) - to the URL "/fraudcheck"
#(4) - with the JSON body that
# * has a field `client.id`
# * has a field `loanAmount` that is equal to `99999`
#(5) - with header `Content-Type` equal to `application/json`
#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}`
#(7) - then the response will be sent with
#(8) - status equal `200`
#(9) - and JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
#(10) - with header `Content-Type` equal to `application/json`
#
#From the Producer perspective, in the autogenerated producer-side test:
#
#(1) - A request will be sent to the producer
#(2) - With the "PUT" method
#(3) - to the URL "/fraudcheck"
#(4) - with the JSON body that
# * has a field `client.id` `1234567890`
# * has a field `loanAmount` that is equal to `99999`
#(5) - with header `Content-Type` equal to `application/json`
#(7) - then the test will assert if the response has been sent with
#(8) - status equal `200`
#(9) - and JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
#(10) - with header `Content-Type` equal to `application/json;charset=UTF-8`

A contract test is a test that verifies that the producer and the consumer will be able to integrate with each other. It doesn’t mean that the functionality will work fine. This distinction is important beacuse you wouldn’t want to duplicate your work by writing a contract per each feature. Contract tests are executed on both sides of the communication and verify whether both parties can still communicate. Their main advantage is that they are fast and reliable.

In this article we will focus on two main types of contract tests: Producer Contract testing and Consumer Driven Contract testing. The main difference between them is the cooperation style of the producer and the consumer.

  • In the Producer Contract testing approach, the producer defines the contracts / writes the contract tests, describes the API and publishes the stubs without any cooperation with its clients. Often this happens when the API is public and the owners of the API don’t even know who exactly is using it. An example can be Spring Initializr that publishes its stubs via Spring Rest Docs tests. The stubs for version 0.5.0.BUILD-SNAPSHOT are available here with the stubs classifier.

  • In the Consumer Driven Contract testing approach, the contracts are suggested by the consumers, in strong cooperation with the producer. The producer knows exactly which consumer defined which contract and which one gets broken when the contract compatibility gets broken. This approach is more common when working with an internal API.

In both cases the contracts can be defined in the repo of the producer (either defined via a DSL or by writing contract tests) or an external repo where all contracts are stored.

Introduction to Maven Nomenclature

Since it’s much easier now to use Spring Cloud Contract by non JVM projects, it’s good to explain the basic terms behind the packaging defaults and introduce the Maven nomenclature.

Tip
Apache Maven is a software project management and comprehension tool. Based on the concept of a project object model (POM), Maven can manage a project’s build, reporting and documentation from a central piece of information. (https://maven.apache.org/)

Part of the following definitions were taken from the Maven Glossary

  • Project: Maven thinks in terms of projects. Everything that you will build are projects. Those projects follow a well defined "Project Object Model". Projects can depend on other projects, in which case the latter are called "dependencies". A project may consistent of several subprojects, however these subprojects are still treated equally as projects.

  • Artifact: An artifact is something that is either produced or used by a project. Examples of artifacts produced by Maven for a project include: JARs, source and binary distributions. Each artifact is uniquely identified by a group id and an artifact ID which is unique within a group.

  • JAR: JAR stands for Java ARchive. It’s a format based on the ZIP file format. Spring Cloud Contract packages the contracts and generated stubs in a JAR file.

  • GroupId: A group ID is a universally unique identifier for a project. While this is often just the project name (eg. commons-collections), it is helpful to use a fully-qualified package name to distinguish it from other projects with a similar name (eg. org.apache.maven). Typically, when published to the Artifact Manager, the GroupId will get slash separated and form part of the URL. E.g. for group id com.example and artifact id application would be /com/example/application/.

  • Classifier: The Maven dependency notation looks as follows: groupId:artifactId:version:classifier. The classifier is additional suffix passed to the dependency. E.g. stubs, sources. The same dependency e.g. com.example:application can produce multiple artifacts that differ from each other with the classifier.

  • Artifact manager: When you generate binaries / sources / packages, you would like them to be available for others to download / reference or reuse. In case of the JVM world those artifacts would be JARs, for Ruby these are gems and for Docker those would be Docker images. You can store those artifacts in a manager. Examples of such managers can be Artifactory or Nexus.

What is Spring Cloud Contract

Spring Cloud Contract is an umbrella project holding solutions that help users in successfully implementing different sorts of contract tests. It comes with two main modules. Spring Cloud Contract Verifier that is used mainly by the producer side and Spring Cloud Contract Stub Runner that is used by the consumer side.

The project allows you to define contracts using:

If you chose the DSL option instead of Rest Docs tests, then, on the producer side, from the contracts:

  • tests are generated via a Maven or Gradle plugin to assert that the contract is met

  • stubs are generated for other projects to reuse

The simplified flow of the producer contract approach, for a JVM application using Spring Cloud Contract with YAML contracts looks as follows.

The producer

  • applies a Maven / Gradle Spring Cloud Contract plugin

  • defines YAML contracts under src/test/resources/contracts/

  • from the contract tests and stubs are generated

  • creates a base class that extends the generated tests and sets up the test context

  • once the tests pass a JAR with stubs classifier is created where contracts and stubs are stored

  • the JAR with stubs classifier gets uploaded to a binary storage

The consumer

  • uses Stub Runner to fetch the stubs of the producer

  • Stub Runner starts in memory HTTP servers, fed with the stubs

  • can execute tests against the stubs

Usage of Spring Cloud Contract and Contract Testing as such gives you:

  • stubs reliability - they were generated only after the tests have passed

  • stubs reusability - they can be downloaded and reused by multiple consumers

What is the current "problem" with Spring Cloud Contract

Distibuted systems are set up from applications written in different languages and frameworks. One of the "problems" with Spring Cloud Contract was that the DSL had to be written in Groovy. Even though the contract didn’t require any special knowledge of the language it became a problem for the non JVM users.

On the producer side, Spring Cloud Contract generates tests in Java or Groovy. Of course it became a problem to use those tests in a non JVM environment. Not only do you need to have Java installed but also tests are generated via a Maven or Gradle plugin which requires usage of these build tools.

Spring Cloud Contract and Polyglot Support

Starting with Edgware.SR2 release train and 1.2.3.RELEASE of Spring Cloud Contract we’ve decided to add features that would allow much wider adoption of Spring Cloud Contract in a non JVM world.

We’ve added support of writing contracts using YAML. YAML is a (yet another) markup language that is not bound to any specific language and is already quite widely used. That should tackle the "problem" of defining contracts using a DSL that is related to JVM.

We’ve introduced Docker images for both the producer and the consumer. All the JVM related logic gets wrapped in Docker container, which means that you don’t even have to have Java installed to generate tests and run the stubs using Stub Runner.

In the following sections we will go through an example of a NodeJS application tested using Spring Cloud Contract. The code was forked from https://github.com/bradtraversy/bookstore and is available under https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs . Our aim is to start generating tests and stubs of an existing application as fast as possible with the least effort.

Spring Cloud Contract on the Producer Side

Let’s clone the simple NodeJS MVC application. It connects to a Mongo DB database to store data about books.

$ git clone https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs
$ cd bookstore

The YAML contracts are available under /contracts folder.

$  ls -al contracts
total 16
drwxr-xr-x   4 mgrzejszczak  staff  128 Feb 13 12:51 .
drwxr-xr-x  20 mgrzejszczak  staff  640 Feb 13 12:51 ..
-rw-r--r--   1 mgrzejszczak  staff  511 Feb 13 12:51 1_shouldAddABook.yml
-rw-r--r--   1 mgrzejszczak  staff  627 Feb 13 12:51 2_shouldReturnListOfBooks.yml

The numerical suffixes tell Spring Cloud Contract that the tests generated from these contracts need to be executed sequentially. The stubs will be stateful, meaning that only after performing a request matched by 1_shouldAddABook will the 2_shouldReturnListOfBooks.yml be available by the stubbed HTTP server.

Important
In the real life example, we would run our NodeJS application in a contract testing mode where calls to the database would be stubbed out and there would be no need for stateful stubs. In this example we want to show how we can benefit from Spring Cloud Contract in no time.

Let’s take a look at one of the stubs:

description: |
  Should add a book
request:
  method: POST
  url: /api/books
  headers:
    Content-Type: application/json
  body: '{
    "title" : "Title",
    "genre" : "Genre",
    "description" : "Description",
    "author" : "Author",
    "publisher" : "Publisher",
    "pages" : 100,
    "image_url" : "https://d213dhlpdb53mu.cloudfront.net/assets/pivotal-square-logo-41418bd391196c3022f3cd9f3959b3f6d7764c47873d858583384e759c7db435.svg",
    "buy_url" : "https://pivotal.io"
  }'
response:
  status: 200

The contract states that if a POST request is sent to /api/books with a header Content-Type: application/json and the aforementioned body, then the response should be 200. Now, before running the contract tests, let’s analyze the Spring Cloud Contract docker image requirements.

Spring Cloud Contract Docker Image

The image is available on DockerHub under SpringCloud org. It searches for contracts under the /contracts folder. The output from running the tests will be available under /spring-cloud-contract/build folder (it’s useful for debugging purposes).

Important
The generated tests will assume that your application is up and running and ready to listen to requests on a given port. That means you have to run it before running the contract tests.

It’s enough for you to mount your contracts, pass the environment variables and the image will:

  • generate the contract tests

  • execute the tests against the provided URL

  • generate the WireMock stubs

  • (optional - turned on by default) publish the stubs to a Artifact Manager

Environment Variables

The Docker image requires some environment variables to point to your running application, to the Artifact manager instance etc.

  • PROJECT_GROUP - your project’s group id. Defaults to com.example.

  • PROJECT_VERSION - your project’s version. Defaults to 0.0.1-SNAPSHOT

  • PROJECT_NAME - artifact id. Defaults to example

  • REPO_WITH_BINARIES_URL - URL of your Artifact Manager. Defaults to http://localhost:8081/artifactory/libs-release-local which is the default URL of Artifactory running locally

  • REPO_WITH_BINARIES_USERNAME - (optional) username when the Artifact Manager is secured

  • REPO_WITH_BINARIES_PASSWORD - (optional) password when the Artifact Manager is secured

  • PUBLISH_ARTIFACTS - if set to true then will publish artifact to binary storage. Defaults to true.

These environment variables are used when tests are executed:

  • APPLICATION_BASE_URL - url against which tests should be executed. Remember that it has to be accessible from the Docker container (e.g. localhost will not work)

  • APPLICATION_USERNAME - (optional) username for basic authentication to your application

  • APPLICATION_PASSWORD - (optional) password for basic authentication to your application

Running Spring Cloud Contract tests on the Producer Side

Since we want to run tests, we could just execute:

$ npm test

however, for learning purposes, let’s split it into pieces:

# Stop docker infra (nodejs, artifactory)
$ ./stop_infra.sh
# Start docker infra (nodejs, artifactory)
$ ./setup_infra.sh

# Kill & Run app
$ pkill -f "node app"
$ nohup node app &

# Prepare environment variables
$ SC_CONTRACT_DOCKER_VERSION="1.2.3.RELEASE"
$ APP_IP="192.168.0.100" # This has to be the IP that is available outside of Docker container
$ APP_PORT="3000"
$ ARTIFACTORY_PORT="8081"
$ APPLICATION_BASE_URL="http://${APP_IP}:${APP_PORT}"
$ ARTIFACTORY_URL="http://${APP_IP}:${ARTIFACTORY_PORT}/artifactory/libs-release-local"
$ CURRENT_DIR="$( pwd )"
$ CURRENT_FOLDER_NAME=${PWD##*/}
$ PROJECT_VERSION="0.0.1.RELEASE"

# Execute contract tests
$ docker run  --rm -e "APPLICATION_BASE_URL=${APPLICATION_BASE_URL}" -e "PUBLISH_ARTIFACTS=true" -e "PROJECT_NAME=${CURRENT_FOLDER_NAME}" -e "REPO_WITH_BINARIES_URL=${ARTIFACTORY_URL}" -e "PROJECT_VERSION=${PROJECT_VERSION}" -v "${CURRENT_DIR}/contracts/:/contracts:ro" -v "${CURRENT_DIR}/node_modules/spring-cloud-contract/output:/spring-cloud-contract-output/" springcloud/spring-cloud-contract:"${SC_CONTRACT_DOCKER_VERSION}"

# Kill app
$ pkill -f "node app"

What will happen is that via bash scripts:

To sum up: It was enough to define the YAML contracts, run the NodeJS application and run the Docker image to generate contract tests, stubs and upload them to Artifactory!

Using Spring Cloud Contract stubs on the consumer side

We’re publishing a spring-cloud/spring-cloud-contract-stub-runner Docker image that will start the standalone version of Stub Runner.

Tip
If you’re ok with running a java -jar command instead of running Docker, you can download a standalone JAR from Maven (e.g. for version 1.2.3.RELEASE) wget -O stub-runner.jar 'https://search.maven.org/remote_content?g=org.springframework.cloud&a=spring-cloud-contract-stub-runner-boot&v=1.2.3.RELEASE'

You can pass any of the following properties as environment variables. The convention is that all the letters should be upper case. The camel case notation should and the dot (.) should be separated via underscore (_). E.g. the stubrunner.repositoryRoot property should be represented as a STUBRUNNER_REPOSITORY_ROOT environment variable.

Let’s assume that we want to run the stubs of the bookstore application on port 9876. Let’s run the Stub Runner Boot application with the stubs.

# Provide the Spring Cloud Contract Docker version
$ SC_CONTRACT_DOCKER_VERSION="1.2.3.RELEASE"
# The IP at which the app is running and Docker container can reach it
$ APP_IP="192.168.0.100"
# Spring Cloud Contract Stub Runner properties
$ STUBRUNNER_PORT="8083"
# Stub coordinates 'groupId:artifactId:version:classifier:port'
$ STUBRUNNER_IDS="com.example:bookstore:0.0.1.RELEASE:stubs:9876"
$ STUBRUNNER_REPOSITORY_ROOT="http://${APP_IP}:8081/artifactory/libs-release-local"
# Run the docker with Stub Runner Boot
$ docker run  --rm -e "STUBRUNNER_IDS=${STUBRUNNER_IDS}" -e "STUBRUNNER_REPOSITORY_ROOT=${STUBRUNNER_REPOSITORY_ROOT}" -p "${STUBRUNNER_PORT}:${STUBRUNNER_PORT}" -p "9876:9876" springcloud/spring-cloud-contract-stub-runner:"${SC_CONTRACT_DOCKER_VERSION}"

What’s happening is that

  • a standalone Spring Cloud Contract Stub Runner application got started

  • it downloaded the stub with coordinates com.example:bookstore:0.0.1.RELEASE:stubs

  • it got downloaded from Artifactory running at http://192.168.0.100:8081/artifactory/libs-release-local

  • after a while Stub Runner will be running on port 8083

  • and the stubs will be running at port 9876

On the server side we built a stateful stub. Let’s use curl to assert that the stubs are setup properly.

# let's execute the first request (no response is returned)
$ curl -H "Content-Type:application/json" -X POST --data '{ "title" : "Title", "genre" : "Genre", "description" : "Description", "author" : "Author", "publisher" : "Publisher", "pages" : 100, "image_url" : "https://d213dhlpdb53mu.cloudfront.net/assets/pivotal-square-logo-41418bd391196c3022f3cd9f3959b3f6d7764c47873d858583384e759c7db435.svg", "buy_url" : "https://pivotal.io" }' http://localhost:9876/api/books
# Now time for the second request
$ curl -X GET http://localhost:9876/api/books
# You will receive contents of the JSON

To sum up: Once the stubs got uploaded, it’s enough to run a Docker image with a couple of environment variables and reuse them in our integration tests, regardless of the language used.

Summary

In this blog post we’ve managed to explain what Contract Tests are and why they are important. We’ve presented how Spring Cloud Contract can be used to generate and execute contract tests. Finally we’ve gone through an example of how one can use Spring Cloud Contract Docker images for the producer and the consumer for a non JVM application.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment