Skip to content

enveryasar/pact-cbt-tutorial

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Contract-Based Testing with Pact.js Tutorial

This repository is an educational material for learning Contract-Based Testing (CBT) using Pact.js. It demonstrates how to implement contract tests between a consumer (front-end) and a provider (back-end API) to ensure they can communicate effectively.

Table of Contents

Introduction

Contract-Based Testing is a method that helps ensure services can communicate with each other as expected. Instead of testing the entire integrated system, we focus on the "contracts" (the agreements) between services.

In this tutorial, we use Pact.js, a JavaScript implementation of the Pact contract testing framework.

Key benefits of Contract-Based Testing:

  • Detect breaking changes before they affect consumers
  • Test services in isolation
  • Ensure compatibility between services
  • Speed up testing and deployment processes

Project Structure

pact-cbt-tutorial/
├── consumer-project/        # The consumer application
│   ├── src/
│   │   └── api.js           # API client used by the consumer
│   ├── tests/
│   │   └── consumer.test.js # Consumer tests that generate the contract
│   └── publish-pact.js      # Script to publish contracts to broker
├── provider-project/        # The provider application
│   ├── src/
│   │   └── provider.js      # Express API that serves data
│   └── tests/
│       └── provider.test.js # Provider tests that verify the contract
└── docker-compose.yml       # Docker setup for Pact Broker

Prerequisites

  • Node.js (v14 or later)
  • npm or yarn
  • Docker and Docker Compose (for running the Pact Broker)

Getting Started

  1. Clone this repository:

    git clone https://github.com/enveryasar/pact-cbt-tutorial.git
    cd pact-cbt-tutorial
  2. Start the Pact Broker using Docker Compose:

    docker-compose up -d

    This will start a Pact Broker at http://localhost:9292.

  3. Install dependencies for both projects:

    cd consumer-project
    npm install
    cd ../provider-project
    npm install

Understanding Contract-Based Testing

What is a Contract?

A contract in this context is a formal agreement between a consumer and a provider about the format of their interactions (requests and responses). The contract includes:

  • The HTTP method (GET, POST, etc.)
  • The endpoint path
  • Request headers and body (if applicable)
  • Expected response status, headers, and body

The Workflow

  1. Consumer Tests: The consumer defines expectations about provider behavior
  2. Contract Generation: Tests generate a contract file in JSON format
  3. Contract Publishing: The contract is published to a Pact Broker
  4. Provider Verification: The provider verifies it can meet the expectations
  5. CI/CD Integration: Automated in your deployment pipeline

Writing Consumer Tests

Consumer tests define the expectations that the consumer has of the provider. They create a mock provider and generate the contract file.

Step 1: Set Up the Pact Mock Provider

const provider = new Pact({
  consumer: 'WebAppConsumer',
  provider: 'UserAPIProvider',
  port: 8081,
  log: path.resolve(process.cwd(), 'logs', 'pact.log'),
  dir: path.resolve(process.cwd(), 'pacts'),
  logLevel: 'INFO',
});

Step 2: Define Interactions

An interaction is a specific request the consumer will make and the response it expects:

provider.addInteraction({
  state: 'a user with ID 1 exists',
  uponReceiving: 'a request for user with ID 1',
  withRequest: {
    method: 'GET',
    path: `/users/${userId}`,
    headers: { Accept: 'application/json' },
  },
  willRespondWith: {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
    body: {
      id: Matchers.integer(userId),
      name: Matchers.like('Alice'),
      email: Matchers.email('alice@example.com'),
    },
  },
});

Step 3: Write the Test

it('should successfully return user data', async () => {
  const response = await fetchUser(provider.mockService.baseUrl, userId);
  expect(response.data.id).toBe(userId);
  expect(typeof response.data.name).toBe('string');
});

Step 4: Verify and Generate Contract

The verify() method ensures all expected interactions were made and generates the contract file:

afterEach(() => provider.verify());

Running Consumer Tests

cd consumer-project
npm test

After running the tests, a contract file will be generated in the pacts directory.

Publishing Contracts

Once the contract is generated, it needs to be published to a Pact Broker, which serves as a repository for contracts.

Using the Publish Script

The publish-pact.js script handles this:

const opts = {
  pactFilesOrDirs: [path.resolve(process.cwd(), 'pacts')],
  pactBroker: 'http://localhost:9292',
  consumerVersion: packageJson.version,
  tags: ['development']
};

new Publisher(opts)
  .publish()
  .then(() => {
    console.log('Pact contract published successfully!');
  });

Publishing the Contract

cd consumer-project
npm run publish

Writing Provider Tests

Provider tests verify that the provider can fulfill the expectations set by the consumer.

Step 1: Set Up the Provider API

beforeAll(() => {
  server = app.listen(port, () => {
    console.log(`Provider API listening on http://localhost:${port}`);
  });
});

afterAll((done) => {
  server.close(done);
});

Step 2: Configure the Verifier

const opts = {
  provider: 'UserAPIProvider',
  providerBaseUrl: `http://localhost:${port}`,
  pactBrokerUrl: 'http://localhost:9292',
  publishVerificationResult: true,
  providerVersion: packageJson.version,
  consumerVersionSelectors: [
    { tag: 'development', latest: true }
  ],
  stateHandlers: {
    'a user with ID 1 exists': () => {
      // Setup code to ensure the state exists
      return Promise.resolve('User data is ready');
    },
  },
};

Step 3: Run the Verification

return new Verifier(opts).verifyProvider();

Running Provider Tests

cd provider-project
npm test

Running the Tests

Complete Workflow

  1. Start the Pact Broker:

    docker-compose up -d
  2. Run consumer tests to generate the contract:

    cd consumer-project
    npm test
  3. Publish the contract to the broker:

    npm run publish
  4. Run provider tests to verify the contract:

    cd ../provider-project
    npm test

Workflow Integration

In a real-world scenario, these steps would be integrated into your CI/CD pipeline:

  1. Consumer CI build:

    • Run consumer tests
    • Generate and publish contracts
    • Tag contracts with environment/branch information
  2. Provider CI build:

    • Verify provider against contracts
    • If successful, deploy the provider
    • Update contract verification status
  3. Deployment gates:

    • Use contract verification status to determine if deployment is safe

Frequently Asked Questions

Q: What is the difference between integration testing and contract testing?

A: Integration testing verifies that two or more components work together by testing them as a group. Contract testing focuses on the boundaries between services and verifies that they can communicate as expected without actually connecting them during the test.

Q: Do I need to run the provider when testing the consumer?

A: No. When testing the consumer, Pact creates a mock provider that simulates the real provider according to the defined expectations. This allows you to test the consumer in isolation.

Q: How do I handle authentication in contract tests?

A: Authentication tokens can be included as part of the request headers in your contract. For provider tests, you might need to set up state handlers that generate valid tokens or mock authentication services.

Q: Can I use contract testing for databases or other non-HTTP dependencies?

A: While Pact is primarily designed for HTTP interactions, you can use similar principles for other protocols. There are also specialized tools for testing database contracts, message queues, etc.

Q: How do I handle evolving contracts without breaking existing consumers?

A: Follow these practices:

  • Make backward-compatible changes when possible
  • Use versioning in your APIs
  • Implement feature toggles
  • Use consumer version selectors to verify specific consumer versions

Q: How detailed should my contracts be?

A: Contracts should be specific enough to catch breaking changes but flexible enough to allow non-breaking changes. Use matchers like Matchers.like() and Matchers.term() to focus on the structure rather than exact values.

Q: What are provider states and why do I need them?

A: Provider states describe the preconditions necessary for a test to run successfully. They ensure the provider is in the correct state to respond to the consumer's request. For example, "a user with ID 1 exists" is a provider state.

Additional Resources

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published