Improve Microservice Testing With Contract Testing
A tutorial on how to conduct better microservices tests and how the open source PactJS library can help test microservices designed in several languages.
Join the DZone community and get the full member experience.
Join For FreeToday, it is strange to find a system built in a "monolithic" way. It is becoming more common, because of the advantages that this involves, to divide it into smaller components that communicate with each other to meet the expected needs.
This means that the functionality is not concentrated in a single point, but it is the collaboration of all the parties that gives meaning to the system.
It is common that each of these parts has programmed unit tests that verify that the component behaves correctly in isolation. However, as we all know, that each module works properly individually does not guarantee that the system behaves properly together.
At this point, it could be thought that some integration tests of the complete system would be the solution, and to a certain extent they are, however, in many cases, it is not easy to perform this type of test either because of its complexity, or because "running" the whole system is not simple, because dependencies, unfinished parts, etc. In short, there are many factors that can make a complete set of integration tests in a microservice-oriented environment highly complex.
In this type of situation, it is where approaches such as contract oriented testing provide greater value. In essence, it is about establishing what the consumer expects to receive before certain requests and subsequently verifying that the producer is indeed sending the expected responses, both in form and in data. These contracts are generated by the mock-up of the server, therefore, they are created before starting the development of the server, so that it is guaranteed that it produces at all times the appropriate answers.
This favors the early detection of errors since these tests do not need to have all the system infrastructure raised, only the provider that we are developing and want to validate. Therefore, at the moment that the contract ceases to be fulfilled by said supplier, we will be informed of the error.
There are several libraries that support this type of testing. In this post, I will focus on the use of PactJS.
Contract Testing With PACT
Pact is an open source framework that facilitates the testing of components based on contracts. Its main advantages are:
Open source
Well documented
Active community
Support for the main languages (Ruby, .NET, Java, JS, PHP, etc.)
"Consumer Driven" Model
Consumer-Driven Contract Testing
When making any change in a system where a provider/consumer architecture is followed, the weakest link in the chain will always be the consumer and the one who, mainly, will suffer the effects of any error.
In any service-oriented system, it must be guaranteed at all times that consumers of these services continue to operate normally. That is why you must pay special attention when maintaining compatibility between both systems. This is, mainly, that the format of the requests and answers is the expected one in each part, respectively.
That is why in certain types of solutions it makes more sense for the client to "take the initiative" when defining the rules of communication between the parties. In practice, this approach does not imply that it is the client who "dictates" the rules, in any case, the agreement must arise from a communication between the parties and capture the agreements that have been made.
How Does it Work
The Pact framework establishes a specific workflow and provides a series of utilities that allows it to be carried out.
The main elements that are part of this flow are:
Expectations (Interactions)
Mock Provider
Pact file
Mock Consumer
Pact Broker (optional)
In summary, as shown in the diagram, the flow could be summarized in two parts: "Play & Record" and "Replay & Verify."
The order of execution would be the following:
Define the "interactions" between client and provider. That is, how the supplier must respond to specific consumer requests.
Create unit tests that "exercise" the interactions defined above.
Launch such unit tests through Pact.
Pact will automatically start a local service that mocks the behavior of the provider. This makes it so that before the requests are launched by our client, the unit tests will return the previously established answers as the service would do in a real production environment.
This will allow us to verify, before deploying in production, that our client is compatible with the supplier's answers.
If the unit tests are correct, a JSON file containing Pact will be generated.
This file must be shared with the provider.* In each project, the team should look for the most appropriate way to perform this action. Pact's broker can be an interesting option.
We start the provider.
On the consumer side, we launch the Pact verifier indicating the location of the file with Pact.
Pact will raise a mock of the consumer on localhost. This will launch the previously defined calls against our provider and will check the answers with the expected ones.
Working with this framework provides the following advantages:
Early detection of errors: We verify the compatibility of our systems before deploying the systems.
Scalable: We will establish a different pact for each customer-supplier relationship.
Parallel work: By mocking both the client and the supplier both parties can work in parallel, thus eliminating dependencies between teams.
Multi-technology: Customers and suppliers can be developed in different languages.
Example
The code shown below would correspond to the implementation of Pact in Javascript.
** All the code is available on GitHub.
Installation
To install PACT in a Node environment we must execute the following command:
npm install @ pact-foundation / pact - save-devs
With this, we can already use Pact from our code. However, in order to execute it, we will need a test runner to launch the pertinent verifications. In this example, and for simplicity, we will use the "Mocha + Chai" pair, although it could have been any other option.
npm install mocha chai - save-dev
Note: This configuration should be done both on the client-side and the server-side. In this case, we assume that both parties are developed on Node.js. Otherwise, you should use the Pact library and corresponding test runners in each case.
In the example on GitHub, can be seen how to perform the verification of a Pact file in the case that the client has developed in Python
Configuration of PACT on the Consumer Side
General properties: Specify the name of the consumer and supplier to facilitate debugging, as well as the name and location of the generated agreement
const provider = new Pact({
consumer: 'Insert Films Client',
provider: 'Films Provider',
port: API_PORT,
log: path.resolve(process.cwd(), 'logs', 'pact.log'),
dir: path.resolve(process.cwd(), 'pacts'),
logLevel: LOG_LEVEL,
spec: 2
})
Definition of Interactions
We specify the interaction and the unit test that exercises it:
describe("Inserting films", () => {
before(() => {
filmService = new FilmsService(endPoint);
//Start mock service
return provider.setup();
})
after(() => {
// Generate pact file
return provider.finalize()
})
describe('When insert the meaning of life', () => {
describe('monty phyton film should be inserted', () => {
var pact_body = {"id": 42,
"Name": "Meaning of life",
"Description": "Comedy",
"Year": 1986}
before(() => {
//Interaction
//Request and expected answer
return provider.addInteraction({
state: 'Empty repository',
uponReceiving: 'Insert meaning of life',
withRequest: {
method: 'POST',
path: '/films/',
headers: {
'Content-Type': 'application/json'
},
body: pact_body
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json'
},
body: pact_body
}
})
})
//Unit test that exercises the expectation
it('film is inserted', () => {
return filmService.insertFilm(42)
.then(response => {
expect(response).to.be.not.null;
expect(response.id).to.equal(42);
});
});
})
});
To generate the corresponding agreement, we would execute the tests:
mocha ./client/test/consumerPact.spec.js - timeout 10000
*A timeout is added for security since we must leave time for the system to raise the mocked server. *
On the one hand, with this, we would get the result of the tests that would verify that the client is compatible with the definition of the contract and on the other, the JSON file that specifies the contract.
Verify on the Provider-Side
The Pact verifier will be responsible for launching the requests against the actual service and checking the answers with the specified ones.
Indicate the endpoint of the deployed provider against which the requests will be launched.
If necessary, specify the URL of the service that will be used to perform the appropriate set up of the system before the test.
Indicate the location, whether physical or HTTP, of the Pact file (previously generated from the client).
let clienteNormal = {
provider:"Films Provider",
providerBaseUrl: 'http://localhost:3000',
providerStatesSetupUrl: 'http://localhost:3000/init',
pactUrls: [path.resolve(__dirname, '../../pacts/films_client-films_provider.json')]
};
new Verifier().verifyProvider(clienteNormal).then(() => {
console.log('success');
process.exit(0);
}).catch((error) => {
console.log('failed', error);
process.exit(1);
});
In the same way, as on the client-side, we would run the tests on the server:
mocha ./api/test/apiPact.spec.js - timeout 10000
Results
In the execution console, we will see the results of the test: "success." In this case, everything went well. We could also find error messages corresponding to the differences found between what is specified in the agreement and the actual answers.
All the code is available on GitHub.
Opinions expressed by DZone contributors are their own.
Comments