Contract Testing with GitHub Actions: A Step-by-Step GuideAutomating Contract Testing in Your CI/CD Pipeline with GitHub Actions

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:

  1. Checkout the repository: Ensures the repository is available in the GitHub Actions runner.
  2. Set up Node.js: Installs the correct version of Node.js.
  3. Install dependencies for consumer tests: Installs the required libraries in the frontend.
  4. Run consumer tests: Executes the Pact tests to generate the contract.
  5. Install dependencies for provider tests: Installs the required libraries in the backend.
  6. 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:

  1. Run the consumer tests to generate the contract.
  2. Publish the contract to a Pact Broker (if configured).
  3. Verify the contract against the provider (backend).
  4. 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