A few months ago our frontend team encountered difficulties during work with the client’s remote backend team. Two groups of people who cannot directly communicate with each other and frequent API changes led to a downturn of the project’s development.
Table of contents:
The API updates were frequent, and because of lack of communication, we were not aware of them until performing checks of the whole business flow.
We’ve lost trust to the API and noticed that we should’ve started the development with something that would ensure us that both producer and consumer sides use the same API.
The question was: How do we know that we implement the same API? which led us to another question: Can we be sure that our test mocks behave like the real REST API?.
We decided to dig into it and try to do something to be sure that we have a comprehensive answer to these questions.
Not a solution
Commonly End-to-End tests (E2E) are considered as tests which side effect is that we know that the APIs on the frontend and backend side are not consistent. The problem is they don’t strictly say that the problem is API inconsistency. Their main task is to check the flow of the usage, not the API itself. They also take time. It’s hassle to set up the whole context and run E2E tests to check if the API is implemented properly.
The one to rule them all
Contract testing is a way to ensure that services communicate with each other with the same API “language”. It’s based on the contract (prepared by the producer or consumer) which firms that both sides implement the same API. Both frontend and backend tests are based on the same contract. Contract testing is the killer of the API version hell.
Backend tests don’t need a whole context of the application set up. They only require endpoints and the stubs of injected services methods.
On the other hand frontend tests are just unit tests with stubs of the backend service endpoints.
I’m gonna make him an offer he can’t refuse
How does look consumer-driven contracts development? The frontend and backend developer create an agreement how the API should look like in the form of the contract file. The basic idea is the contract is being written as a part of consumer tests. It’s worth noting that contract defines minimal set of request/response fields that should be present during the communication so if you add new fields they won’t break the contract.
PACT
PACT aids developers to achieve this. The major advantage is it’s well supported by Angular, and it’s contract files, which are contract base for generating producer tests, can be shared with JVM application.
Sharing pacts between consumers and producer
PACT provides a solution to store the contract files called PACT Broker. It is a repository for publishing and retrieving pacts with REST API.
Unfortunately, we had no time to work with another new repository, so we decided to prepare a simple flow on Jenkins and share pacts over git repository instead of running broker.
The following diagram shows our concept of sharing pacts without the broker:
Spring Cloud Contract
Spring Cloud Contract is a set of tools that supports consumer-driven contracts in Spring applications. The project is focused on the custom DSL solution, fortunately, they also provide support for PACTs, so we are able to work together with non-JVM consumer-like Angular frontend application flawlessly.
Test generation
Our task was to implement the API for sending a simple message and returning the id of the message. The given contract looks as below:
{
"consumer": {
"name": "frontend-app"
},
"provider": {
"name": "backend-app"
},
"interactions": [
{
"description": "POST new message",
"providerState": "provider accepts message",
"request": {
"method": "POST",
"path": "/message",
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"body": {
"message": "Sample message"
}
},
"response": {
"status": 201,
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"body": {
"id": "25e3ae11-d294-4a69-9421-2816df07b531"
},
"matchingRules": {
"$.body": {
"match": "type"
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "2.0.0"
}
}
}
Let’s assume that generated pact is already in the producer’s repository src/test/resources/pacts/messaging.json
Preparing build.gradle
We need to add dependencies and enable a gradle plugin:
buildscript {
...
dependencies {
...
classpath("org.springframework.cloud:spring-cloud-contract-gradle-plugin:${springCloudContractVersion}")
classpath("org.springframework.cloud:spring-cloud-contract-pact:${springCloudContractVersion}")
...
}
}
...
apply plugin: 'spring-cloud-contract'
...
dependencies {
...
testImplementation("au.com.dius:pact-jvm-provider-spring_2.12:${pactVersion}") // we need this one to use PACT provider annotations
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier:${springCloudContractVersion}") // tests use the SpringCloudContractAssertions
...
}
After that, we need to configure contracts
section in gradle which is used to generate tests:
import org.springframework.cloud.contract.verifier.config.TestFramework
contracts {
targetFramework = TestFramework.JUNIT // it's default value, you can use SPOCK instead
contractsPath = "pacts/" // default directory is `contracts/`
baseClassForTests = 'com.inspeerity.article.contract.MessagingContractMocks' // here we specify which class will be extended by the generated tests
basePackageForTests = 'com.inspeerity.article.contract' // base package for generated tests
}
Base class for tests
We need to implement base class for tests. It stores information about test definitions we want to run and defines service method stubs.
Let’s create MessagingContractMocks
as we defined it as baseClassForTests
:
package com.inspeerity.article.contract;
import ...
@RunWith(SpringRestPactRunner.class)
@WebMvcTest(MessageController.class)
@PactFolder("contracts/")
@Provider("backend-app")
abstract class MessagingContractMocks {
@TestTarget
public final MockMvcTarget target = new MockMvcTarget();
@Autowired
private MessageControler messageControler;
@Before
public void setupBefore() {
MockitoAnnotations.initMocks(this);
target.setControllers(featureController);
}
@MockBean
private MessageSender messageSender;
@State("provider accepts message")
public void aRequestToGETFeatures() {
}
}
You may notice that:
- We use a dedicated pact spring library, so we can use
SpringRestPactRunner
instead ofPactRunner
as the Junit’s runner. It allows us to use spring test annotations. - We’ve put
@WebMvcTest
annotation because for contract testing purposes all we need in the application context are web related components and we’ll mock beans from other layers. @Provider
annotation defines we are interested in contractsbackend-app
provider.- If you have more than one consumer or use PACT Proker you may also need to define
@Consumer
annotation to define which contract you want to use. - Contract tests require
@TestTarget
annotatedTarget
interface implementation, which implementation should throw an exception on unexpected response.MockMvcTarget
is out-of-the-box implementation that verifies controller responses. It must be defined if you useSpringRestPactRunner
and all tested controllers need to be set, otherwise, they won’t be checked and test will fail withNot found
error. - We use
@MockBean
annotation to createMessageSender
bean mock. It’s used by our controller so we will need to stub it’s method later. - We define
@State
annotated method. It’s our place to define our mocks and stubs for the chosen contract state.
Generating test class
To check if test generates test class run:
./gradlew generateContractTests
Here is your generated test class!
Contract tests are based on response types, not particular values (they don’t test the logic). That’s why the generated tests check value types instead of values.
Try to run your tests:
./gradlew test
As you see the test fails:
Description explains with details that the:
Content-Type
header is missing- status is
404
instead of expected201
- response type is
test/plain
instead of expectedapplication/json
Implementation
We need to create MessageController
:
package com.inspeerity.article.contract.messaging;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MessageControler {
private final MessageSender messageSender;
@Autowired
public MessageController(MessageSender messageSender) {
this.messageSender = messageSender;
}
@PostMapping(
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE
)
@ResponseStatus(HttpStatus.CREATED)
public MessageDto postMessage(@RequestBody MessageCommand messageCommand) {
return new MessageDto(messageSender.sendMessage(messageCommand.getMessage()));
}
}
Of course you need to create also classes:
- input data type:
MessageCommand
with fieldmessage
ofString
type MessageDto
with one fieldid
ofUUID
typeMessageSender
annotated with@Service
with public methodsendMessage(String message)
returning UUID
You don’t need to create any logic in MessageSender
just return some UUID. In fact, for tutorial purposes, we will just care about its method stub.
Do not forget to stub the service method:
@State("provider accepts message")
public void postNewMessage() {
when(messageSender.sendMessage("Sample message")) // we need to stub service method we used in controller
.thenReturn(UUID.fromString("25e3ae11-d294-4a69-9421-2816df07b531")); // this data comes from pact file
}
Now if you run:
./gradlew test
You will see the test passed:
What have we gained?
Our problem was the API inconsistency. By using contract tests we get rid of the hassle and time-consuming E2E tests. The important thing is the backend developers must be aware that if they break the contract by i.e. changing the type of a field the test will fail. On the other hand, if frontend developers need a new endpoint or modify the old data format, they need to prepare a new contract.
Would you like to talk with our experts about custom software solutions for your business?
Q: What difficulties did the frontend team encounter during work with the client’s remote backend team?
The frontend team encountered difficulties due to the lack of direct communication with the backend team and frequent API changes. This led to a downturn in the project’s development.
Q: How did the team address these difficulties?
The team decided to implement contract testing as a way to ensure that both the frontend and backend services were communicating with the same API “language”. They also used Spring Cloud Contract and PACT to aid in this process.
Q: What is contract testing?
Contract testing is a way to ensure that services communicate with each other with the same API “language”. It is based on a contract (prepared by the producer or consumer) that states that both sides implement the same API. Both frontend and backend tests are based on the same contract.