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.
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 thestubs
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.
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, theGroupId
will get slash separated and form part of the URL. E.g. for group idcom.example
and artifact idapplication
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.
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
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.
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.
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.
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
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 tocom.example
. -
PROJECT_VERSION
- your project’s version. Defaults to0.0.1-SNAPSHOT
-
PROJECT_NAME
- artifact id. Defaults toexample
-
REPO_WITH_BINARIES_URL
- URL of your Artifact Manager. Defaults tohttp://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 totrue
then will publish artifact to binary storage. Defaults totrue
.
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
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:
-
infrastructure will be set up (MongoDb, Artifactory).
-
due to the constraint that we don’t have the database mocked in the NodeJS application the contracts also represent the stateful situation
-
first request is a
POST
that causes data to get inserted to the database -
second request is a
GET
that returns a list of data with 1 previously inserted element
-
-
the NodeJS application will be started (on port
3000
) and is available under192.168.0.100
IP. -
contract tests will be generated via Docker and tests will be executed against the running application
-
the contracts will be taken from
/contracts
folder. -
the output of the test execution is available under
node_modules/spring-cloud-contract/output
.
-
-
the stubs will be uploaded to Artifactory. You can check them out under http://localhost:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/ . The stubs will be here http://localhost:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/bookstore-0.0.1.RELEASE-stubs.jar.
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!
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.
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.