contract-test-framework
You are contract-test-framework - a specialized skill for consumer-driven contract testing between SDKs and APIs, ensuring compatibility and preventing breaking changes through automated verification.
Overview
This skill enables AI-powered contract testing including:
- Generating Pact consumer contracts from SDK usage
- Configuring Pact Broker for contract management
- Provider verification against consumer contracts
- Can-i-deploy safety checks before releases
- Breaking change detection and alerting
- Webhook integration for automated verification
- Support for bidirectional contract testing
Prerequisites
- Node.js 18+ or Python 3.8+
- Pact library for your SDK language
- Pact Broker (PactFlow recommended) or self-hosted
- CI/CD pipeline access
- Consumer SDK and provider API access
Capabilities
1. Consumer Contract Generation for SDKs
Generate contracts from SDK tests:
// tests/contracts/user-api.pact.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { MyServiceSDK } from '@company/myservice-sdk';
const { like, eachLike, regex, uuid, datetime, integer } = MatchersV3;
const provider = new PactV3({
consumer: 'myservice-typescript-sdk',
provider: 'myservice-api',
logLevel: 'info'
});
describe('MyService SDK Contracts', () => {
describe('Users API', () => {
it('should get user by ID', async () => {
const expectedUser = {
id: uuid(),
email: like('user@example.com'),
name: like('John Doe'),
createdAt: datetime("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"),
status: regex(/active|inactive|pending/, 'active')
};
await provider
.given('a user with ID exists', { userId: 'user-123' })
.uponReceiving('a request to get user by ID')
.withRequest({
method: 'GET',
path: '/api/v1/users/user-123',
headers: {
'Accept': 'application/json',
'Authorization': regex(/Bearer .+/, 'Bearer test-token')
}
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: expectedUser
});
await provider.executeTest(async (mockServer) => {
const sdk = new MyServiceSDK({
baseUrl: mockServer.url,
accessToken: 'test-token'
});
const user = await sdk.users.get('user-123');
expect(user).toBeDefined();
expect(user.email).toMatch(/@/);
});
});
it('should list users with pagination', async () => {
await provider
.given('users exist')
.uponReceiving('a request to list users')
.withRequest({
method: 'GET',
path: '/api/v1/users',
query: {
page: '1',
limit: '20'
}
})
.willRespondWith({
status: 200,
body: {
data: eachLike({
id: uuid(),
email: like('user@example.com'),
name: like('User Name')
}),
pagination: {
page: integer(1),
limit: integer(20),
total: integer(100),
hasMore: like(true)
}
}
});
await provider.executeTest(async (mockServer) => {
const sdk = new MyServiceSDK({ baseUrl: mockServer.url });
const response = await sdk.users.list({ page: 1, limit: 20 });
expect(response.data).toBeInstanceOf(Array);
expect(response.pagination.page).toBe(1);
});
});
it('should create a new user', async () => {
await provider
.given('the system is ready')
.uponReceiving('a request to create a user')
.withRequest({
method: 'POST',
path: '/api/v1/users',
headers: {
'Content-Type': 'application/json',
'Authorization': regex(/Bearer .+/, 'Bearer test-token')
},
body: {
email: like('newuser@example.com'),
name: like('New User'),
password: like('securePassword123')
}
})
.willRespondWith({
status: 201,
body: {
id: uuid(),
email: like('newuser@example.com'),
name: like('New User'),
createdAt: datetime("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
}
});
await provider.executeTest(async (mockServer) => {
const sdk = new MyServiceSDK({
baseUrl: mockServer.url,
accessToken: 'test-token'
});
const user = await sdk.users.create({
email: 'newuser@example.com',
name: 'New User',
password: 'securePassword123'
});
expect(user.id).toBeDefined();
});
});
it('should handle 404 for non-existent user', async () => {
await provider
.given('user does not exist', { userId: 'nonexistent' })
.uponReceiving('a request for non-existent user')
.withRequest({
method: 'GET',
path: '/api/v1/users/nonexistent'
})
.willRespondWith({
status: 404,
body: {
error: {
code: like('USER_NOT_FOUND'),
message: like('User not found')
}
}
});
await provider.executeTest(async (mockServer) => {
const sdk = new MyServiceSDK({ baseUrl: mockServer.url });
await expect(sdk.users.get('nonexistent'))
.rejects
.toThrow('User not found');
});
});
});
});
2. Multi-SDK Contract Testing
Test contracts for multiple SDK implementations:
# pact-config.yaml
consumers:
- name: myservice-typescript-sdk
language: typescript
version: ${GIT_COMMIT}
branch: ${GIT_BRANCH}
- name: myservice-python-sdk
language: python
version: ${GIT_COMMIT}
branch: ${GIT_BRANCH}
- name: myservice-java-sdk
language: java
version: ${GIT_COMMIT}
branch: ${GIT_BRANCH}
provider:
name: myservice-api
baseUrl: http://localhost:3000
broker:
url: https://your-broker.pactflow.io
token: ${PACT_BROKER_TOKEN}
publishResults: true
verification:
enablePending: true
wipPactsSince: '2024-01-01'
consumerVersionSelectors:
- matchingBranch: true
- mainBranch: true
- deployedOrReleased: true
3. Provider Verification
Verify API against all SDK contracts:
// tests/contracts/provider-verification.ts
import { Verifier } from '@pact-foundation/pact';
import { startServer, stopServer, resetDatabase } from '../test-utils';
describe('Provider Verification', () => {
beforeAll(async () => {
await startServer();
});
afterAll(async () => {
await stopServer();
});
it('should verify all SDK contracts', async () => {
const verifier = new Verifier({
provider: 'myservice-api',
providerBaseUrl: 'http://localhost:3000',
// Pact Broker configuration
pactBrokerUrl: process.env.PACT_BROKER_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
// Provider version
providerVersion: process.env.GIT_COMMIT || '1.0.0',
providerVersionBranch: process.env.GIT_BRANCH || 'main',
// Consumer selection
consumerVersionSelectors: [
{ matchingBranch: true },
{ mainBranch: true },
{ deployedOrReleased: true }
],
// State handlers for test setup
stateHandlers: {
'a user with ID exists': async (params) => {
await resetDatabase();
await db.users.create({
id: params.userId,
email: 'user@example.com',
name: 'John Doe'
});
},
'users exist': async () => {
await resetDatabase();
await db.users.createMany([
{ id: 'user-1', email: 'user1@example.com', name: 'User 1' },
{ id: 'user-2', email: 'use