Consumer-driven contract testing skill using Pact, covering consumer tests, provider verification, Pact Broker integration, and CI/CD workflows.
npx skills add https://github.com/PramodDutta/qaskills --skill Contract Testing with Pact
You are an expert QA engineer specializing in consumer-driven contract testing with Pact. When the user asks you to write, review, or debug Pact contract tests, follow these detailed instructions.
Consumer Side Provider Side
┌─────────────┐ ┌─────────────┐
│ Consumer │ │ Provider │
│ Test │ │ Test │
│ │ │ │
│ 1. Define │ Pact File │ 3. Verify │
│ expected │ ─────────────> │ against │
│ behavior │ │ running │
│ │ │ service │
│ 2. Generate │ │ │
│ Pact │ │ 4. Publish │
│ file │ │ results │
└─────────────┘ └─────────────┘
│ │
│ ┌───────────────┐ │
└──────>│ Pact Broker │<──────┘
│ │
│ Stores pacts │
│ Tracks │
│ verification │
│ can-i-deploy │
└───────────────┘
consumer-service/
src/
clients/
user-api-client.ts
product-api-client.ts
models/
user.model.ts
tests/
contract/
user-api.consumer.pact.spec.ts
product-api.consumer.pact.spec.ts
pacts/ <-- Generated Pact files
consumer-user-service.json
jest.config.ts
package.json
provider-service/
src/
routes/
users.routes.ts
services/
user.service.ts
tests/
contract/
user-api.provider.pact.spec.ts
utils/
provider-states.ts
jest.config.ts
package.json
npm install --save-dev @pact-foundation/pact
// src/clients/user-api-client.ts
import axios, { AxiosInstance } from 'axios';
export interface User {
id: string;
email: string;
name: string;
role: string;
}
export interface CreateUserRequest {
email: string;
name: string;
password: string;
}
export class UserApiClient {
private http: AxiosInstance;
constructor(baseUrl: string) {
this.http = axios.create({ baseUrl });
}
async getUser(id: string): Promise<User> {
const response = await this.http.get(`/api/users/${id}`);
return response.data;
}
async createUser(data: CreateUserRequest): Promise<User> {
const response = await this.http.post('/api/users', data);
return response.data;
}
async listUsers(page: number = 1): Promise<{ data: User[]; total: number }> {
const response = await this.http.get(`/api/users?page=${page}`);
return response.data;
}
async deleteUser(id: string): Promise<void> {
await this.http.delete(`/api/users/${id}`);
}
}
// tests/contract/user-api.consumer.pact.spec.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import path from 'path';
import { UserApiClient } from '../../src/clients/user-api-client';
const { like, eachLike, string, uuid, integer, regex } = MatchersV3;
const provider = new PactV3({
consumer: 'frontend-app',
provider: 'user-service',
dir: path.resolve(process.cwd(), 'tests/pacts'),
logLevel: 'warn',
});
describe('User API Consumer Contract', () => {
describe('GET /api/users/:id', () => {
it('should return a user when the user exists', async () => {
// Arrange: Define the expected interaction
provider
.given('a user with ID user-123 exists')
.uponReceiving('a request to get a user by ID')
.withRequest({
method: 'GET',
path: '/api/users/user-123',
headers: {
Accept: 'application/json',
},
})
.willRespondWith({
status: 200,
headers: {
'Content-Type': 'application/json',
},
body: {
id: like('user-123'),
email: regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, '[email protected]'),
name: string('John Doe'),
role: regex(/^(admin|user|viewer)$/, 'user'),
},
});
// Act & Assert: Execute against the mock provider
await provider.executeTest(async (mockServer) => {
const client = new UserApiClient(mockServer.url);
const user = await client.getUser('user-123');
expect(user.id).toBe('user-123');
expect(user.email).toBeDefined();
expect(user.name).toBeDefined();
expect(user.role).toBeDefined();
});
});
it('should return 404 when the user does not exist', async () => {
provider
.given('no user with ID nonexistent exists')
.uponReceiving('a request to get a non-existent user')
.withRequest({
method: 'GET',
path: '/api/users/nonexistent',
headers: {
Accept: 'application/json',
},
})
.willRespondWith({
status: 404,
headers: {
'Content-Type': 'application/json',
},
body: {
message: string('User not found'),
statusCode: integer(404),
},
});
await provider.executeTest(async (mockServer) => {
const client = new UserApiClient(mockServer.url);
await expect(client.getUser('nonexistent')).rejects.toThrow();
});
});
});
describe('POST /api/users', () => {
it('should create a new user', async () => {
const requestBody = {
email: '[email protected]',
name: 'New User',
password: 'SecurePass123!',
};
provider
.given('the email [email protected] is not taken')
.uponReceiving('a request to create a new user')
.withRequest({
method: 'POST',
path: '/api/users',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: requestBody,
})
.willRespondWith({
status: 201,
headers: {
'Content-Type': 'application/json',
},
body: {
id: uuid('550e8400-e29b-41d4-a716-446655440000'),
email: string('[email protected]'),
name: string('New User'),
role: string('user'),
},
});
await provider.executeTest(async (mockServer) => {
const client = new UserApiClient(mockServer.url);
const user = await client.createUser(requestBody);
expect(user.id).toBeDefined();
expect(user.email).toBe('[email protected]');
expect(user.name).toBe('New User');
});
});
});
describe('GET /api/users', () => {
it('should return a paginated list of users', async () => {
provider
.given('there are users in the system')
.uponReceiving('a request to list users')
.withRequest({
method: 'GET',
path: '/api/users',
query: { page: '1' },
headers: {
Accept: 'application/json',
},
})
.willRespondWith({
status: 200,
headers: {
'Content-Type': 'application/json',
},
body: {
data: eachLike({
id: uuid(),
email: string('[email protected]'),
name: string('Test User'),
role: string('user'),
}),
total: integer(10),
},
});
await provider.executeTest(async (mockServer) => {
const client = new UserApiClient(mockServer.url);
const result = await client.listUsers(1);
expect(result.data.length).toBeGreaterThan(0);
expect(result.total).toBeGreaterThanOrEqual(result.data.length);
});
});
});
});
// tests/contract/user-api.provider.pact.spec.ts
import { Verifier } from '@pact-foundation/pact';
import path from 'path';
import { app } from '../../src/app'; // Your Express/Fastify app
describe('User Service Provider Contract Verification', () => {
let server: any;
beforeAll(async () => {
server = app.listen(3001);
});
afterAll(async () => {
server.close();
});
it('should validate the expectations of the frontend-app consumer', async () => {
const verifier = new Verifier({
providerBaseUrl: 'http://localhost:3001',
provider: 'user-service',
// Option 1: From local pact files
pactUrls: [
path.resolve(process.cwd(), '../consumer-service/tests/pacts/frontend-app-user-service.json'),
],
// Option 2: From Pact Broker
// pactBrokerUrl: 'https://your-broker.pactflow.io',
// pactBrokerToken: process.env.PACT_BROKER_TOKEN,
// consumerVersionSelectors: [
// { mainBranch: true },
// { deployedOrReleased: true },
// ],
// Provider states setup
stateHandlers: {
'a user with ID user-123 exists': async () => {
// Seed the database with test data
await db.users.create({
id: 'user-123',
email: '[email protected]',
name: 'John Doe',
role: 'user',
});
return { description: 'User user-123 created' };
},
'no user with ID nonexistent exists': async () => {
// Ensure the user does not exist
await db.users.deleteMany({ id: 'nonexistent' });
return { description: 'Ensured user does not exist' };
},
'the email [email protected] is not taken': async () => {
await db.users.deleteMany({ email: '[email protected]' });
return { description: 'Email [email protected] is available' };
},
'there are users in the system': async () => {
await db.users.createMany({
data: [
{ id: 'user-1', email: '[email protected]', name: 'User 1', role: 'user' },
{ id: 'user-2', email: '[email protected]', name: 'User 2', role: 'admin' },
],
});
return { description: 'Seeded 2 users' };
},
},
// Publish verification results
publishVerificationResult: process.env.CI === 'true',
providerVersion: process.env.GIT_COMMIT || '1.0.0',
providerVersionBranch: process.env.GIT_BRANCH || 'main',
// Logging
logLevel: 'warn',
});
await verifier.verifyProvider();
});
});
package com.example.consumer;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import au.com.dius.pact.consumer.MockServer;
import au.com.dius.pact.core.model.V4Pact;
import au.com.dius.pact.core.model.annotations.Pact;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "user-service")
public class UserApiConsumerPactTest {
@Pact(consumer = "frontend-app")
public V4Pact getUserByIdPact(PactDslWithProvider builder) {
return builder
.given("a user with ID user-123 exists")
.uponReceiving("a request to get user by ID")
.path("/api/users/user-123")
.method("GET")
.headers("Accept", "application/json")
.willRespondWith()
.status(200)
.headers(Map.of("Content-Type", "application/json"))
.body(new PactDslJsonBody()
.stringType("id", "user-123")
.stringMatcher("email", "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$", "[email protected]")
.stringType("name", "John Doe")
.stringMatcher("role", "^(admin|user|viewer)$", "user")
)
.toPact(V4Pact.class);
}
@Test
@PactTestFor(pactMethod = "getUserByIdPact")
void testGetUserById(MockServer mockServer) {
UserApiClient client = new UserApiClient(mockServer.getUrl());
User user = client.getUser("user-123");
assertThat(user.getId()).isEqualTo("user-123");
assertThat(user.getEmail()).isNotEmpty();
assertThat(user.getName()).isNotEmpty();
}
}
package com.example.provider;
import au.com.dius.pact.provider.junit5.PactVerificationContext;
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
import au.com.dius.pact.provider.junitsupport.*;
import au.com.dius.pact.provider.junitsupport.loader.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Provider("user-service")
@PactBroker(
url = "${PACT_BROKER_URL}",
authentication = @PactBrokerAuth(token = "${PACT_BROKER_TOKEN}")
)
public class UserApiProviderPactTest {
@LocalServerPort
private int port;
@BeforeEach
void setUp(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", port));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void verifyPact(PactVerificationContext context) {
context.verifyInteraction();
}
@State("a user with ID user-123 exists")
void userExists() {
userRepository.save(new User("user-123", "[email protected]", "John Doe", "user"));
}
@State("no user with ID nonexistent exists")
void userDoesNotExist() {
userRepository.deleteById("nonexistent");
}
@State("the email [email protected] is not taken")
void emailAvailable() {
userRepository.deleteByEmail("[email protected]");
}
}
# Using Pact CLI
pact-broker publish ./tests/pacts \
--consumer-app-version=$(git rev-parse --short HEAD) \
--branch=$(git rev-parse --abbrev-ref HEAD) \
--broker-base-url=https://your-broker.pactflow.io \
--broker-token=${PACT_BROKER_TOKEN}
# Check if consumer can deploy
pact-broker can-i-deploy \
--pacticipant=frontend-app \
--version=$(git rev-parse --short HEAD) \
--to-environment=production \
--broker-base-url=https://your-broker.pactflow.io \
--broker-token=${PACT_BROKER_TOKEN}
# Check if provider can deploy
pact-broker can-i-deploy \
--pacticipant=user-service \
--version=$(git rev-parse --short HEAD) \
--to-environment=production \
--broker-base-url=https://your-broker.pactflow.io \
--broker-token=${PACT_BROKER_TOKEN}
# After successful deployment
pact-broker record-deployment \
--pacticipant=user-service \
--version=$(git rev-parse --short HEAD) \
--environment=production \
--broker-base-url=https://your-broker.pactflow.io \
--broker-token=${PACT_BROKER_TOKEN}
name: Consumer Contract Tests
on: [push, pull_request]
jobs:
consumer-contract-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run test:contract
- name: Publish Pacts
if: github.ref == 'refs/heads/main'
run: |
npx pact-broker publish tests/pacts \
--consumer-app-version=${{ github.sha }} \
--branch=${{ github.ref_name }} \
--broker-base-url=${{ secrets.PACT_BROKER_URL }} \
--broker-token=${{ secrets.PACT_BROKER_TOKEN }}
- name: Can I Deploy?
if: github.ref == 'refs/heads/main'
run: |
npx pact-broker can-i-deploy \
--pacticipant=frontend-app \
--version=${{ github.sha }} \
--to-environment=production \
--broker-base-url=${{ secrets.PACT_BROKER_URL }} \
--broker-token=${{ secrets.PACT_BROKER_TOKEN }}
name: Provider Contract Verification
on:
push:
branches: [main]
# Webhook trigger from Pact Broker when new pacts are published
repository_dispatch:
types: [pact-changed]
jobs:
provider-verification:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Verify Provider Contracts
run: npm run test:provider-contract
env:
PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
GIT_COMMIT: ${{ github.sha }}
GIT_BRANCH: ${{ github.ref_name }}
CI: true
- name: Can I Deploy?
if: github.ref == 'refs/heads/main'
run: |
npx pact-broker can-i-deploy \
--pacticipant=user-service \
--version=${{ github.sha }} \
--to-environment=production \
--broker-base-url=${{ secrets.PACT_BROKER_URL }} \
--broker-token=${{ secrets.PACT_BROKER_TOKEN }}
import { MatchersV3 } from '@pact-foundation/pact';
const {
like, // Match by type, not exact value
eachLike, // Array where each element matches the example
string, // String type matcher
integer, // Integer type matcher
decimal, // Decimal type matcher
boolean, // Boolean type matcher
uuid, // UUID format matcher
datetime, // Date-time format matcher
date, // Date format matcher
time, // Time format matcher
regex, // Regex pattern matcher
fromProviderState, // Value from provider state
arrayContaining, // Array contains these elements (in any order)
atLeastOneLike, // Array with at least N elements matching
} = MatchersV3;
// Examples
const body = {
id: uuid(),
name: string('John'),
age: integer(30),
score: decimal(95.5),
active: boolean(true),
email: regex(/^.+@.+\..+$/, '[email protected]'),
createdAt: datetime("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", '2024-01-15T10:30:00.000Z'),
tags: eachLike('tag1'),
address: like({
street: string('123 Main St'),
city: string('Anytown'),
}),
};
like() and regex() make contracts resilient.mainBranch and deployedOrReleased pacts.git rev-parse --short HEAD as the version.10. Automate webhook triggers -- Configure the Pact Broker to trigger provider verification on new pacts.
10. Not testing error responses -- Consumers must handle errors correctly.
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Use when implementing any feature or bugfix, before writing implementation code
Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
Expert guidance for systematic backtesting of trading strategies. Use when developing, testing, stress-testing, or validating quantitative trading strategies. Covers "beating ideas to death" methodology, parameter robustness testing, slippage modeling, bias prevention, and interpreting backtest results. Applicable when user asks about backtesting, strategy validation, robustness testing, avoiding overfitting, or systematic trading development.
Cloud laboratory platform for automated protein testing and validation. Use when designing proteins and needing experimental validation including binding assays, expression testing, thermostability measurements, enzyme activity assays, or protein sequence optimization. Also use for submitting experiments via API, tracking experiment status, downloading results, optimizing protein sequences for better expression using computational tools (NetSolP, SoluProt, SolubleMPNN, ESM), or managing protein design workflows with wet-lab validation.
This skill should be used for time series machine learning tasks including classification, regression, clustering, forecasting, anomaly detection, segmentation, and similarity search. Use when working with temporal data, sequential patterns, or time-indexed observations requiring specialized algorithms beyond standard ML approaches. Particularly suited for univariate and multivariate time series analysis with scikit-learn compatible APIs.
Take pramoddutta/contract testing with pact from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npm, npx.
Without those the skill loads but fails at the first command.