| name | contract-tester |
| description | Contract testing expertise covering the Pact framework, consumer-driven contract design, provider verification, Pact Broker setup, can-i-deploy workflow, contract versioning strategies, webhook integration for CI, API evolution testing, and GraphQL contract patterns.
Use when the user asks about contract tester, contract tester best practices, or needs guidance on contract tester implementation.
Do NOT use when the user needs a different specialized skill or is asking about an unrelated technology domain.
|
| license | Apache-2.0 |
| metadata | {"author":"foundry-skills","version":"1.0.0","tags":"testing best-practices api-design","category":"testing-quality","subcategory":"test-methodology","depends":"","disclaimer":"none","difficulty":"intermediate"} |
Contract Tester
Core Philosophy
Contract testing ensures that services can communicate with each other without running full integration tests across every service simultaneously. Instead of testing the integration directly, each side (consumer and provider) tests against a shared contract. If both sides satisfy the contract, they can communicate successfully. This enables independent deployment and testing of microservices.
Consumer-Driven Contracts (CDC)
The Flow
1. CONSUMER defines what it needs from the provider (the contract)
2. Consumer tests are run, generating a contract (Pact file)
3. Contract is shared via Pact Broker
4. PROVIDER verifies it can fulfill the contract
5. If both pass -> safe to deploy independently
Why Consumer-Driven?
Provider-driven: "Here's my API, figure out what you need"
Problem: Provider changes can break consumers without knowing
Consumer-driven: "Here's what I need from you"
Benefit: Provider knows exactly what consumers depend on
Benefit: Provider can safely change anything NOT in a contract
Benefit: Breaking changes are caught before deployment
Pact Framework
Consumer Side (JavaScript)
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike, string, integer, iso8601DateTime } = MatchersV3;
const provider = new PactV4({
consumer: 'OrderService',
provider: 'UserService',
logLevel: 'warn',
});
describe('User API Contract', () => {
test('get user by ID', async () => {
await provider
.addInteraction()
.given('user 42 exists')
.uponReceiving('a request for user 42')
.withRequest('GET', '/api/users/42', (builder) => {
builder.headers({ 'Accept': 'application/json' });
})
.willRespondWith(200, (builder) => {
builder
.headers({ 'Content-Type': 'application/json' })
.({
: (),
: (),
: (),
: (),
: (),
});
})
.( (mockServer) => {
client = (mockServer.);
user = client.();
(user.).();
(user.).();
(user.).();
});
});
(, () => {
provider
.()
.()
.()
.(, )
.(, {
builder.({
: (),
: (),
});
})
.( (mockServer) => {
client = (mockServer.);
(client.())..();
});
});
(, () => {
provider
.()
.()
.()
.(, , {
builder.({ : , : });
})
.(, {
builder.({
: ({
: (),
: (),
: (),
}),
: (),
: (),
: (),
});
})
.( (mockServer) => {
client = (mockServer.);
result = client.({ : , : });
(result..).();
(result.).();
});
});
});
Provider Side (JavaScript)
import { Verifier } from '@pact-foundation/pact';
import { app } from '../src/app';
describe('Pact Verification', () => {
let server: any;
beforeAll(async () => {
server = app.listen(0);
});
afterAll(() => server.close());
test('validates the expectations of OrderService', async () => {
const port = server.address().port;
const verifier = new Verifier({
providerBaseUrl: `[reference URL]
provider: 'UserService',
pactBrokerUrl: ENV_CONFIG_VALUE,
pactBrokerToken: ENV_CONFIG_VALUE,
// Provider states: set up data for each interaction
stateHandlers: {
'user 42 exists': async () => {
await seedDatabase({
users: [{ id: 42, name: 'Alice Smith', email: 'alice@example.com', tier: 'premium' }]
});
},
'user 999 does not exist': async () => {
await clearDatabase();
},
'multiple users exist': async () => {
await seedDatabase({
users: Array.from({ length: 25 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}.
Provider Side (Python)
import pytest
from pact_python.verifier import Verifier
def test_provider_honors_pact_with_order_service():
verifier = Verifier(
provider="UserService",
provider_base_url="[reference URL]",
)
output, logs = verifier.verify_pacts(
broker_url=environment-variables["PACT_BROKER_URL"],
broker_token=environment-variables["PACT_BROKER_TOKEN"],
publish_version=environment-variables.get("GIT_SHA", "local"),
publish_verification_results=environment-variables.get("CI") == "true",
provider_states_setup_url="[reference URL]",
consumer_version_selectors=[
{"mainBranch": True},
{"deployedOrReleased": True},
],
enable_pending=True,
)
assert output == 0, f"Pact verification failed:\n{logs}"
Pact Broker
Setup with Docker
version: '3'
services:
pact-broker:
image: pactfoundation/pact-broker:latest
ports:
- "9292:9292"
environment:
PACT_BROKER_DATABASE_URL: postgres://pact:pact@postgres/pact
PACT_BROKER_BASIC_AUTH_USERNAME: admin
PACT_BROKER_BASIC_AUTH_PASSWORD: admin
PACT_BROKER_ALLOW_PUBLIC_READ: "true"
depends_on:
- postgres
postgres:
image: postgres:16
environment:
POSTGRES_USER: pact
POSTGRES_PASSWORD: pact
POSTGRES_DB: pact
volumes:
- pact-data:/var/lib/postgresql/data
volumes:
pact-data:
Publishing Pacts
# Publish consumer pact to broker
npx pact-broker publish ./pacts \
--consumer-app-version=$(git rev-parse HEAD) \
--branch=$(git branch --show-current) \
--broker-base-url=$PACT_BROKER_URL \
--broker-token=$PACT_BROKER_TOKEN
# Tag with environment after deployment
npx pact-broker create-version-tag \
--pacticipant=OrderService \
--version=$(git rev-parse HEAD) \
--tag=production
Can-I-Deploy Workflow
The can-i-deploy command checks whether it is safe to deploy a particular version of a service.
# Check if OrderService can be deployed to production
npx pact-broker can-i-deploy \
--pacticipant=OrderService \
--version=$(git rev-parse HEAD) \
--to-environment=production \
--broker-base-url=$PACT_BROKER_URL
# Output:
# COMPUTER SAYS YES
# All required verification results are published and successful
# Or:
# COMPUTER SAYS NO
# UserService (v1.2.3) has not verified the pact published by OrderService (v2.0.0)
CI Pipeline Integration
name: Deploy Pipeline
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
- name: Publish pacts
run: |
npx pact-broker publish ./pacts \
--consumer-app-version=${{ github.sha }} \
--branch=${{ github.ref_name }} \
--broker-base-url=${{ secrets.PACT_BROKER_URL }} \
--broker-token=${{ secrets.PACT_BROKER_TOKEN }}
can-i-deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Check deployment safety
run: |
npx pact-broker can-i-deploy \
--pacticipant=OrderService \
--version=${{ github.sha }} \
--to-environment=production \
--broker-base-url=${{ secrets.PACT_BROKER_URL }} \
--broker-token=${{ secrets.PACT_BROKER_TOKEN }}
deploy:
needs: can-i-deploy
runs-on: ubuntu-latest
steps:
-
Contract Versioning
Handling Breaking Changes
1. Consumer adds a new field to its expectation
-> Provider must now return this field
-> Provider verification will fail until updated
2. Provider wants to remove a field
-> Check Broker: is any consumer using this field?
-> If yes: coordinate removal (add new version, migrate consumers)
-> If no: safe to remove
3. Provider wants to change a field type
-> This is a breaking change
-> Use API versioning (v1 -> v2) or additive changes
Pending Pacts
Enable pending pacts so new consumers don't break provider CI:
1. OrderService (new consumer) publishes pact
2. UserService provider verification runs
3. Since OrderService pact is "pending" (not yet verified successfully):
- Provider can see the new expectations
- But failure doesn't break the provider build
4. Provider makes changes to satisfy the new pact
5. Provider verification succeeds
6. Pact is no longer "pending" - future failures WILL break the build
Webhook Integration
# Configure webhook: trigger provider verification when consumer pact changes
npx pact-broker create-webhook \
"[reference URL]" \
--request=POST \
--header="Authorization: Bearer ${GITHUB_TOKEN}" \
--header="Content-Type: application/json" \
--data='{"event_type":"pact_changed","client_payload":{"pact_url":"${pactbroker.pactUrl}"}}' \
--consumer=OrderService \
--provider=UserService \
--contract-content-changed \
--broker-base-url=$PACT_BROKER_URL
API Evolution Testing
Additive Changes (Non-Breaking)
{ "id": 42, "name": "Alice", "email": "alice@example.com" }
{ "id": 42, "name": "Alice", "email": "alice@example.com", "avatar_url": "[reference URL]" }
Deprecation Workflow
1. Provider marks field as deprecated in documentation
2. Check Pact Broker: which consumers depend on this field?
3. Notify consumer teams
4. Consumer teams update their code and remove field from contracts
5. Once no consumer contract references the field -> safe to remove
GraphQL Contracts
await provider
.addInteraction()
.given('user 42 exists')
.uponReceiving('a GraphQL query for user 42')
.withRequest('POST', '/graphql', (builder) => {
builder
.headers({ 'Content-Type': 'application/json' })
.jsonBody({
query: `query GetUser($id: ID!) {
user(id: $id) {
id
name
email
orders {
id
total
}
}
}`,
variables: { id: '42' }
});
})
.willRespondWith(200, (builder) => {
builder.jsonBody({
data: {
user: {
id: string('42'),
name: string('Alice Smith'),
email: string('alice@example.com'),
orders: eachLike({
id: string('ord_1'),
total: (),
}),
},
},
});
})
.( (mockServer) => {
client = (mockServer. + );
result = client.();
(result..).();
(result...).();
});
Best Practices
- Test consumer expectations, not full API surface: Only include fields your consumer actually uses
- Use matchers, not exact values:
like(42) not 42 -- the shape matters, not specific values
- Name interactions descriptively: "a request for user 42 when the user exists"
- Set up provider states properly: Each interaction should be reproducible
- Run can-i-deploy before every deployment: Make it a mandatory CI gate
- Use pending pacts: Let new consumers be added without breaking provider CI
- Tag deployments in the broker: Track which versions are in each environment
- Consumer owns the contract: Providers serve consumers, not the other way around
When to Use
Use this skill when:
- Designing or implementing contract tester solutions
- Reviewing or improving existing contract tester approaches
- Making architectural or implementation decisions about contract tester
- Learning contract tester patterns and best practices
- Troubleshooting contract tester-related issues
Do NOT use this skill when:
- The question is about a fundamentally different technology domain
- A more specific sibling skill covers the exact topic needed
- The user needs a complete hands-on tutorial rather than expert guidance
Output Format
# Contract Tester Analysis
## Context Assessment
[Situation summary and constraints]
## Recommended Approach
[Primary recommendation with rationale]
## Implementation Steps
1. [Step with specific details]
2. [Step with specific details]
3. [Step with specific details]
## Trade-offs and Considerations
- [Key trade-off 1]
- [Key trade-off 2]
## Next Steps
- [Immediate action item]
- [Follow-up action item]
Example
Input: "Help me implement contract tester for a medium-scale production application"
Output: A structured analysis covering current state assessment, recommended contract tester approach with specific patterns, implementation roadmap with milestones, and risk mitigation strategies tailored to the application scale and constraints.
Edge Cases
- Legacy system integration: When contract tester must coexist with legacy approaches, provide a gradual migration path rather than a complete rewrite
- Scale mismatch: When the solution complexity exceeds the project scale, recommend a simpler approach and note when to revisit
- Team skill gaps: When the team lacks experience with the recommended approach, include learning resources and simpler alternatives
- Conflicting requirements: When constraints conflict (e.g., performance vs. maintainability), explicitly state the trade-off and recommend based on stated priorities