As codebases mature, they grow in complexity. As time goes on, higher complexity increases the cost of changing and scaling a system.
Scalability is an essential feature of enterprise software. End-to-end integration testing is not a very scalable process, especially when you are dealing with multiple systems. As you add more teams and components to systems, the linear increase results in an exponential increase in other factors, including environments, build time, risk associated with new changes, and developer idle time.
In this tutorial, you will learn how contract testing using Pact to verify contracts makes integration testing easier and more scalable. The tutorial will cover testing HTTP integration between dependent API endpoints, and you will also learn how to use CircleCI to run automated Pact tests in a continuous integration (CI) pipeline.
Prerequisites
This is what you need to complete this tutorial:
- Node.js installed on your system
- A CircleCI account
- A GitHub account (and an understanding of Git)
- Sound knowledge of JavaScript and Nodejs
- A clone of the test application from GitHub.
If you’re unsure how to clone a repository from GitHub, the Git documentation is a good resource.
Note: The focus of this tutorial will be on the configuration of consumer and provider tests with Pact and not API application development.
What is Pact?
Pact is a code-first tool for contract testing HTTP and integration messages between microservices. Pact ensures that two different systems communicate with one another through a contract.
A contract is a document that contains the agreed-upon standards between two dependent services, typically a consumer and a provider. A consumer is an application such as a frontend application. A provider can be compared to an an application, like a web server, that provides data to the consumer. In software, a provider agrees to a contract, promising to behave in a predictable way, so long as the consumer provides the necessary input. In this context, “pact” is a reference to the contract between a consumer and a provider.
Contract testing is a method of testing integration points in applications in isolation. It ensures that the messages apps send or receive conform to an agreed-upon understanding documented in a “contract” file.
Why do we need contract testing?
When it comes to software testing priorities, unit testing comes below integration testing in the testing pyramid. This position in the testing pyramid means you generally should have fewer integration tests than you do unit tests, because they are slow to write, slow to execute, and often expensive to maintain.
Integration testing checks that a group of modules work together as designed. It’s important that a codebase includes integration tests - units that work in isolation may not work well together. For example, after testing a crank and a gear separately, you need to test whether the crank moves the gear.
Note: Integration testing aims to find interfacing issues between modules. In contrast, end-to-end (E2E) testing tries to determine whether an application’s flow from start to finish is behaving as expected.
Unlike E2E testing, contract testing using Pact does not require communication with multiple systems. Contract testing does not involve dependencies with all other systems, making it faster than E2E tests. Pact makes integration tests easier to maintain because you do not need to understand entire systems to write the tests.
Contract testing examines the system’s points of interface rather than the entire system to determine the individual needs of each integration point.
If an authentication service in a microservices architecture changes and no other teams are notified, the entire system will possibly run into errors as the endpoints could return invalid data. If a team uses Pact for integration testing, you can use the contract to run tests against the service and know when it has changed. Changes to expected responses or payload structure are detected so you can modify your applications accordingly.
In contrast to other contract testing tools:
- Pact generates language-agnostic acceptance contracts in the form of JSON Pact files.
- Pact is a contract testing platform driven by customers. Pact’s consumer code is in charge of creating the contract.
- The Pact Broker provides auto-generated documentation and allows consumers and providers to be cross-tested.
- Features related to provider states are in charge of managing test data and sequencing tests in pacts.
Configuring Pact in a microservice
This section covers how to set up consumer tests in a few straightforward steps.
Mocking the provider using Pact
First, you’ll work on testing a consumer. You can find this setup for the consumer tests in the example project at tests/consumer.spec.js
The first code block in the example:
- Imports
Pact
and then instantiates it. - Specifies the directory to store the Pact contracts in.
- Specifies the
mock port
the mock server will run on. - Sets the names of the
consumer
andprovider
.
Run:
// tests/consumer.spec.js
const path = require("path");
const TodoManager = require("../client");
const { PactV3, MatchersV3 } = require("@pact-foundation/pact");
// Set up a server to run the consumer tests against
const server = require("../index")
describe("Test consumer", () => {
// pact mock server url
const mock_port = 1234;
const mock_server_url = "http://127.0.0.1:" + mock_port;
// pact instance
const provider = new PactV3({
consumer: "todo_consumer",
provider: "todo_provider",
port: mock_port,
dir: path.resolve(process.cwd(), "tests", "pacts"),
logLevel: "DEBUG",
});
// Next code block
});
Creating an interaction
After mocking the provider, the example defines a sample response object similar to what is expected as a response from the mock server. The interactions are then determined by specifying the expected request and response, which must align with what was specified.
// Expected response from the provider
const EXPECTED_BODY = {
title: "Learn React Native",
description:
"React Native is a framework for building native apps using React.",
isCompleted: false,
urgency: "high",
id: 1,
};
it("test: getAllTodos", () => {
// interaction
provider
// Set up expected request
.uponReceiving("a GET request to get all todos")
.withRequest({
method: "GET",
path: "/todos",
})
// Set up expected response
.willRespondWith({
status: 200,
headers: {
"Content-Type": "application/json",
},
body: MatchersV3.eachLike(EXPECTED_BODY),
});
// Verification
});
Note: Pact leverages a mock server to match HTTP requests and generate responses from Pact files.
Send a request to the mock server and verify it
After configuring the interactions, the example sends a request to the mock server. It then verifies that the response received is similar to the expected response.
// Verify request
return provider.executeTest(() => {
// Make request to the mock server
const todos = new TodoManager(mock_server_url);
return todos.getAllTodos().then((response) => {
// Verify response
expect(response).toEqual([EXPECTED_BODY]);
});
}).finally(() => {
server.close();
});
These tests are in the example project, so you can run them to follow along. After the repository is cloned, cd
into the directory and install the dependencies:
npm install
Next, run these consumer tests:
npm run test:consumer
When the consumer tests run successfully, a Pact contract file named **"todo\_consumer-todo\_provider.json"**
is created in the tests/pacts
folder.
And just like that, you are able to verify the Pact consumer tests. The APIs provide the expected responses to the consumer applications. Next, you will learn how to test the provider integration points with Pact.
Provider contract testing
Provider contract testing ensures that a provider’s actual behavior corresponds to its documented contract. Provider tests are usually found on the provider side, which is often a backend web service like an API. This contrasts with consumer tests, in which a consumer is similar to a frontend application.
Unlike consumer contract testing, provider contract testing is straightforward. Provider testing uses a Pact verifier instance to verify requests to existing endpoints.
Creating a Pact verifier instance
The example project has provider tests in tests/provider.spec.js
:
// tests/provider.spec.js
const { Verifier } = require("@pact-foundation/pact");
const path = require("path");
// Set up a server to run the consumer tests against
const server = require("../index")
describe("Pact Verification", () => {
it("verifies the provider", () => {
const options = {
provider: "todo_provider",
providerBaseUrl: "http://localhost:5000",
disableSSLVerification: true,
logLevel: 'DEBUG',
pactUrls: [
path.resolve(
process.cwd(),
"tests",
"pacts",
"todo_consumer-todo_provider.json"
),
],
};
// Verify the provider
});
});
Note: You must provide the pact’s location and other options such as the provider’s base URL and name to verify the provider.
Verifying the provider
// Verify the provider with the pact file, then stop the server
return new Verifier(options)
.verifyProvider()
.then(() => {
console.log('Pact Verification Complete!');
}).finally(() => {
server.close();
});
This code block shows how to verify a provider using a Pact file by calling the .verifyProvider
method. When this is successful, the Pact provider server is closed.
Note: Pact starts the provider and consumer servers before executing the tests.
In the cloned repository, run the provider tests:
npm run test:provider
Automating contract tests with CI
Previous sections focused on setting Pact in a microservice and successfully writing tests for the consumer and provider. To extend the usefulness of the Pact tests, you can automate the provider and consumer tests to run in a CI/CD pipeline on every change to your application using CircleCI.
Start by creating a .circleci/config.yml
file at the root of your project. Add this configuration:
// .circleci/config.yml
version: '2.1'
orbs:
node: circleci/node@5.0.2
jobs:
test_microservices:
executor:
name: node/default
tag: '18.7.0'
steps:
- checkout
- node/install-packages:
cache-path: ~/project/node_modules
override-ci-command: npm install
- run: sudo npm install -g npm@latest
- run: npm run test:consumer
- run: npm run test:provider
workflows:
test_contracts:
jobs:
- test_microservices
This configuration file uses the CircleCI Node orb with node version 18.7.0
and then installs dependencies, including npm. Once dependencies are installed, it runs the consumer and provider tests. The installation steps cache the node modules, saving time over installing node modules consecutively.
If you do not have a GitHub repository for your project, create a new GitHub repository and commit your changes. Push your changes to GitHub. Then set up an automated test pipeline on CircleCI.
Log in to your CircleCI account dashboard. On the Projects tab, find your GitHub project and click Create Project.
Select your Git hosting platform, then give your project a name and link it to your repository. CircleCI will automatically detect the configuration file.
When you click Create Project, the pipeline will not automatically start executing. Push a small, meaningless commit (like adding a newline) to the project to trigger your first pipeline run. CircleCI will then run your tests, resulting in a successfully executed pipeline.
With a green build to show off, you are now ready to start writing contract tests on your own and integrating them to a CI/CD pipeline.
Conclusion
In this tutorial, you learned what Pact is and how to use it to write integration tests for microservices using contract testing. You also learned how to write consumer and provider tests, configuring Pact when using both consumer systems (frontend applications) and provider tests (API applications). Finally, you learned how to set up a pipeline on CircleCI to automate the execution of the tests to make integration testing a consistent part of your development workflow.
I hope you enjoyed reading this tutorial as much as I did making it. Until next time, keep learning!