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.
- Introduction
- Project Structure
- Prerequisites
- Getting Started
- Understanding Contract-Based Testing
- Writing Consumer Tests
- Publishing Contracts
- Writing Provider Tests
- Running the Tests
- Workflow Integration
- Frequently Asked Questions
- Additional Resources
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
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
- Node.js (v14 or later)
- npm or yarn
- Docker and Docker Compose (for running the Pact Broker)
-
Clone this repository:
git clone https://github.com/enveryasar/pact-cbt-tutorial.git cd pact-cbt-tutorial -
Start the Pact Broker using Docker Compose:
docker-compose up -d
This will start a Pact Broker at http://localhost:9292.
-
Install dependencies for both projects:
cd consumer-project npm install cd ../provider-project npm install
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
- Consumer Tests: The consumer defines expectations about provider behavior
- Contract Generation: Tests generate a contract file in JSON format
- Contract Publishing: The contract is published to a Pact Broker
- Provider Verification: The provider verifies it can meet the expectations
- CI/CD Integration: Automated in your deployment pipeline
Consumer tests define the expectations that the consumer has of the provider. They create a mock provider and generate the contract file.
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',
});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'),
},
},
});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');
});The verify() method ensures all expected interactions were made and generates the contract file:
afterEach(() => provider.verify());cd consumer-project
npm testAfter running the tests, a contract file will be generated in the pacts directory.
Once the contract is generated, it needs to be published to a Pact Broker, which serves as a repository for contracts.
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!');
});cd consumer-project
npm run publishProvider tests verify that the provider can fulfill the expectations set by the consumer.
beforeAll(() => {
server = app.listen(port, () => {
console.log(`Provider API listening on http://localhost:${port}`);
});
});
afterAll((done) => {
server.close(done);
});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');
},
},
};return new Verifier(opts).verifyProvider();cd provider-project
npm test-
Start the Pact Broker:
docker-compose up -d
-
Run consumer tests to generate the contract:
cd consumer-project npm test
-
Publish the contract to the broker:
npm run publish
-
Run provider tests to verify the contract:
cd ../provider-project npm test
In a real-world scenario, these steps would be integrated into your CI/CD pipeline:
-
Consumer CI build:
- Run consumer tests
- Generate and publish contracts
- Tag contracts with environment/branch information
-
Provider CI build:
- Verify provider against contracts
- If successful, deploy the provider
- Update contract verification status
-
Deployment gates:
- Use contract verification status to determine if deployment is safe
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.
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.
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.
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.
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
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.
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.