Playing With Spring Cloud Contract
In this article, take a look at Spring Cloud Contract and put some ideas into code.
Join the DZone community and get the full member experience.Join For Free
In a previous post, we saw how new needs arose in the field of testing derived from the evolution of application architectures.
Through a simple example, we established concepts such as consumer, producer, and service and showed that just as important as testing the functionalities in consumer and producer independently is, so also is ensuring that the interaction between them both is right.
We introduced the concept of Contract Testing, which we delved into in another post, which allowed us to get familiar with the different approaches and tools.
Now, with all the information to hand, it’s time to put all those ideas into code. We will do it step by step, starting from the example in the first post, which we can download from here. Remember that here we highlighted the problem that we might encounter: an application that fails in production despite passing all the unit and integration tests.
1. Define the Contract
We will start by defining the agreement: we will write the specification that consumer and producer have to comply with for the communication to work properly. In Spring Cloud Contract the term contract is used. It can be defined in different ways (groovy, yaml, java, kotlin) and we have chosen
yaml because for this example, we thought it would be easy to read.
For our use case we define the following contract:
In this case, we are establishing the following agreement:
For a request with: a username and a date (whose format we also specify), the response will be:
status 200 and a
JSON (whose content we establish through a file).
This contract must be accessible to the producer. In this case, and for simplicity, it will be in the producer folder
Once we have defined the contract, we will have to complete the implementation that complies with it and the necessary tests to verify it. In this example we were already starting from the implementation, so let’s get on with the tests!
2. Producer: Configure Dependencies in the pom.xml
We modify the
pom.xml by adding the Spring Cloud Contract Verifier dependency and the
spring-cloud-contract-maven-plugin. With the latter we will automatically achieve:
- Generation of tests that verify that our producer complies with the contract
- Creation of a stub that will allow the consumer to generate a WireMock (which will comply with the contract) against which to run its tests
Producer | pom.xml
As shown in the
pom.xml in addition to the dependencies, we have configured some of the plugin properties:
- We have indicated that the test framework will be JUnit 5
- We have indicated that the package that will contain the test base class will be
What’s this about the test base class? According to the specification, we must generate a base class for the autogenerated tests to extend. This class must contain all the information necessary for executing the tests (for example, we could configure mocks of some beans, populate the database with specific data for tests …).
For this example we have created a very simple base class whose only responsibility will be to start up the application context. We have defined the contract in the
contracts/worklogs folder, therefore (based on the documentation that indicates that the name is inferred from the names of the last two folders), the class is called
Producer | WorklogsBase.java
The plugin supports the configuration of other parameters as explained in the plugin documentation, such as: where the contracts are, how to generate the different elements, etc.
3. Producer: Create and Run Tests
As we mentioned in the previous section, with this plugin we can automatically generate the tests that ensure that the producer complies with the contract. To do this we execute the command
./mvnw clean test and we see that:
- The producer test classes are generated
- The tests are run
- In addition, a
.jaris created (which for now we leave parked)
What about the self-generated tests? They are in the
generated-test-sources folder. In this specific case, as we have a single folder, a single class is generated:
Producer | WorklogsTest.java
As we can see, there is a test method for the definition of the contract. If we had more than one, then we would have a test for each of them.
In the test that has been generated, we see how an
assert is made to check that the response
statusCode is as expected. We also see how it is verified that the type of response is a
json and how the
.json file has been parsed (which we referenced in the specification) to make the necessary
asserts that ensure the response is as expected.
And from eclipse?
I personally use eclipse in my development, so I am interested in being able to run it from the IDE. Obviously we need it to be generated first and we have already seen how we should do it through
./mvnw clean test. But if I have not changed the contract and it is not necessary to regenerate the tests and I am also developing and I want to pass all the tests, how do I do it? As they are autogenerated classes, it is necessary to add the
generated-test-sources folders to the buildpath. For example, in this case:
4. Consumer: Configure Dependencies
Unlike the producer, the tests in the consumer related to the contract are not generated automatically. But we are not alone: remember that at the same time that the producer tests were created, a
.jar was also generated with the stub that will allow us to simulate the calls to the producer from the consumer tests.
In our case, the jar is:
timeReports-producer-0.0.1-SNAPSHOT-stubs.jar which we can find in the producer
In this case, having both projects locally, if instead of executing the command
./mvnw clean test (in the producer) we run
./mvnw clean install we will have the said jar directly in our local maven repository, with which we can configure our consumer to access it.
In order to have access to it, we add the following dependency in the
Consumer | pom.xml
Also, we must not forget the Spring Cloud Contract dependency to be able to execute the added stub.
Consumer | pom.xml
5. Consumer: Create and Run Tests
Once the dependencies are added, we can create the WireMock based on that stub and create our tests. We will do it with the annotation
@AutoConfigureStubRunner where we indicate our stub so it will be downloaded and registered automatically in the Wiremock. An example might be:
Consumer | ReportsServiceContractTest.java
With this, we would already have the communication between the two tested, making sure that if at any time there was a modification of the contract in either of the two components, the tests of the other would fail. Let’s see it.
Are We Able to Deploy in Production With the Certainty That Everything Works?
This was the problem we encountered in the previous post: despite having tested consumer and producer, we were not able to know if anything was wrong until we reached production. Will we be able to detect it now?
Suppose we make the same change that we proposed in that post:
Producer | WorklogController.java
We run the tests and indeed they fail: both the unitary one we have and the self-generated one. And why is this? Because we have actually modified the contract. We update the contract and correct the tests that fail:
./mvnw clean test and now all our tests pass. Right! Let’s see how the autogenerated test has changed.
Producer | WorklogsTest.java
As we have changed the contract, we must send the update to the consumer, so we execute
./mvnw clean install in the producer and run the consumer tests with
./mvnw clean test.
What’s going on? Although the unit tests from before continue to work correctly, the new added test fails: it shows us that something has changed in the contract, so the application will not work. Right! Goal achieved: we have detected the problem before deployment to production.
We modify the consumer implementation:
Consumer | ReportsService.java
We run the tests, and we see that when changing the implementation, the unit tests have also stopped working (logically). Therefore, all we need to do is correct them.
As we have seen in the example, it is important that when the contract changes, both consumer and producer are aware of that change.
In this case, as it is from the producer where the change is made, it is important that the consumer receives the new specification through the stub (if not, the tests will continue to pass). In our example, it is simple because we have everything locally. It helps us to explain the concept in a simple way, but we should not forget that it does not reflect reality, where often, different people are working on one or the other, without needing to have projects locally. There are many ways to organize the code and therefore there are different solutions, which should be analyzed depending on the project and its requirements. The most important questions to be answered would be:
- Where do we place the contracts? Could they be in the project itself (as in the example) or maybe it would be better if they were in their own GitHub repository?
- How do we manage the producer stub? Could we deploy it in a maven repo?
- How do we manage versioning? Will it be the same as that of the producer or will it be independent?
We are not going to go into evaluating these and many other things, which should be taken into account when putting it into practice because the answer will be it depends. It depends on the project, on the organization of the teams… In the Spring Cloud Contract documentation, there are different recommendations and examples that could be useful.
Published at DZone with permission of Jessica Aguado. See the original article here.
Opinions expressed by DZone contributors are their own.