Introduction: The Importance of Contract Testing in Modern Development
In the world of modern software development, especially with microservices and APIs, ensuring that services communicate effectively and consistently is critical. APIs are the backbone of these communications, and when a provider (backend) makes changes, it can break the consumers (frontends or other services). To avoid these breaking changes, contract testing comes into play.
Contract testing is a method of ensuring that the contract between a service consumer and a provider remains valid and unbroken. In this blog post, we’ll explore how to automate contract testing using GitHub Actions, enabling you to detect breaking changes early in your CI/CD pipeline.
Why Automate Contract Testing with GitHub Actions?
When working with services that depend on APIs, especially in a microservices architecture, you want to be sure that changes to one service don’t break others. Manually running contract tests is cumbersome and prone to human error. Automating these tests ensures that contract verification is part of your continuous integration (CI) process, allowing you to catch any issues before they make it to production.
GitHub Actions is an ideal platform for automating contract testing. It offers powerful automation tools, flexible workflows, and easy integration into your existing projects hosted on GitHub. With GitHub Actions, you can set up automated contract testing to run whenever code changes are pushed or merged, ensuring that all services adhere to the expected API contracts.
Step 1: Setting Up Pact for Contract Testing
To implement contract testing, we’ll use Pact, a popular tool for consumer-driven contract testing. Pact helps verify that a provider (backend service) adheres to a contract defined by a consumer (frontend service or another microservice). The first step is to install Pact in both your consumer and provider projects.
For the consumer (frontend), install Pact:
npm install @pact-foundation/pact @pact-foundation/pact-web --save-dev
For the provider (backend), install Pact:
npm install @pact-foundation/pact --save-dev
Step 2: Writing Consumer Tests
The consumer is responsible for defining the contract. Suppose we have a React frontend that fetches user data from a Node.js backend. The contract specifies that the frontend expects an API response with a specific structure, such as:
{
"id": "123",
"name": "John Doe",
"email": "john@example.com"
}
Here’s how you might write a Pact consumer test for the React frontend:
const { Pact } = require("@pact-foundation/pact");
const fetchUser = require("./fetchUser"); // Function that calls the API
describe("Pact Consumer Test", () => {
const provider = new Pact({
consumer: "ReactFrontend",
provider: "NodeBackend",
port: 1234,
});
before(() => provider.setup());
after(() => provider.finalize());
it("should return user data as expected by the frontend", async () => {
await provider.addInteraction({
state: "User with ID 123 exists",
uponReceiving: "a request for user data",
withRequest: {
method: "GET",
path: "/users/123",
headers: { Accept: "application/json" },
},
willRespondWith: {
status: 200,
headers: { "Content-Type": "application/json" },
body: {
id: "123",
name: "John Doe",
email: "john@example.com",
},
},
});
const response = await fetchUser("123");
expect(response.name).toEqual("John Doe");
expect(response.email).toEqual("john@example.com");
});
});
This test sets up an interaction that defines what the frontend expects when it requests user data. The contract (Pact file) will be generated automatically after the test runs.
Step 3: Writing Provider Verification Tests
Once the contract is defined by the consumer, the provider (backend service) needs to verify that it adheres to the contract. This is done by running provider verification tests against the contract generated by the consumer.
Here’s an example of a provider test in the Node.js backend:
const { Verifier } = require("@pact-foundation/pact");
const path = require("path");
describe("Pact Provider Verification", () => {
it("should validate the expectations of the ReactFrontend", async () => {
const opts = {
providerBaseUrl: "http://localhost:3000",
pactUrls: [
path.resolve(__dirname, "../pacts/reactfrontend-nodebackend.json"),
],
};
await new Verifier(opts).verifyProvider();
});
});
This test verifies that the backend adheres to the contract by responding to requests in the expected format.
Step 4: Setting Up GitHub Actions for Contract Testing
Now that you have your consumer and provider tests ready, it's time to automate the process using GitHub Actions.
1. Create a Workflow File
In your repository, create a .github/workflows/contract-tests.yml
file to define your GitHub Actions workflow.
name: Contract Testing
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
contract-tests:
runs-on: ubuntu-latest
services:
backend:
image: node:14
ports:
- 3000:3000
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Install dependencies (Consumer)
working-directory: ./frontend
run: npm install
- name: Run Pact Consumer Tests
working-directory: ./frontend
run: npm run test:pact
- name: Install dependencies (Provider)
working-directory: ./backend
run: npm install
- name: Run Pact Provider Verification
working-directory: ./backend
run: npm run test:pact-provider
2. Define Steps for Consumer and Provider Tests
In the above workflow, we’ve defined the following steps:
- Checkout the repository: Ensures the repository is available in the GitHub Actions runner.
- Set up Node.js: Installs the correct version of Node.js.
- Install dependencies for consumer tests: Installs the required libraries in the frontend.
- Run consumer tests: Executes the Pact tests to generate the contract.
- Install dependencies for provider tests: Installs the required libraries in the backend.
- Run provider verification: Verifies the contract on the backend to ensure that the API adheres to the expected contract.
Step 5: Integrating with a Pact Broker
To manage contracts between different services, you can use a Pact Broker. A Pact Broker allows you to publish and retrieve contracts across different services, helping to keep track of which versions of the services are compatible with each other.
You can publish your contract to a Pact Broker after running the consumer tests by adding the following step to your GitHub Actions workflow:
- name: Publish Pact to Broker
run: |
npm run pact:publish
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
Make sure to set up the PACT_BROKER_BASE_URL
and PACT_BROKER_TOKEN
secrets in your GitHub repository settings.
Step 6: Running the Workflow and Validating Contracts
Once you push your changes to the repository, GitHub Actions will automatically run the defined workflow. It will:
- Run the consumer tests to generate the contract.
- Publish the contract to a Pact Broker (if configured).
- Verify the contract against the provider (backend).
- Ensure that any changes in the backend do not break the contract.
This setup allows you to automate contract testing and ensure that API communication between services remains consistent.
Conclusion: Streamlining Contract Testing with GitHub Actions
Contract testing is an essential practice when working with microservices or distributed systems. It helps ensure that changes in one service do not break others that rely on it. By automating contract testing with GitHub Actions, you can seamlessly integrate this process into your CI/CD pipeline.
With Pact and GitHub Actions, you’ll be able to catch potential breaking changes early in development, avoiding costly bugs in production. Set up consumer and provider tests, configure your GitHub Actions workflow, and leverage a Pact Broker to manage contracts across your services. This will not only improve the reliability of your services but also accelerate the development process by providing early feedback on potential issues.
References
- Blog - Scott Logic - Introduction to contract testing
- Lambdatest - Contract Testing Guide: Definition, Process, and Examples
- Medium - Introduction to Contract Testing
- Medium - Introduction to Contract Testing with Pact — the Basics
- Pactflow - Introduction to Contract Testing with Pact — the Basics
- Book - Building Microservices: Designing Fine-Grained Systems
- Book - Microservices Patterns: With examples in Java by Chris Richardson
- Book - Testing Microservices with Mountebank by Brandon Byars
- Book - API Testing and Development with Postman by Dave Westerveld
- Book - Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation by Jez Humble and David Farley
- Book - REST API Design Rulebook: Designing Consistent RESTful Web Service Interfaces by Mark Masse
- Book - Effective Software Testing: A Developer's Guide by Maurizio Aniche
- Book - Fundamentals of Software Architecture: An Engineering Approach by Mark Richards