Skip to main content سوق المهارات اكتشف واستكشف مهارات الذكاء الاصطناعي التي بناها المجتمع.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
نسخ Promptعرض تفاصيل Prompt يتجاوز الأمر المباشر Prompt المخصّص للمراجعة. افحص المصدر قبل تشغيله.
npx skills add https://github.com/PramodDutta/qaskills --skill contract-testing-with-pactيبقى الأمر في سطر واحد. مرّر أفقيًا لمراجعته كاملًا قبل النسخ.
تفضّل نسخة محلية؟ نزّل الملفات المتاحة حاليًا لدى SkillsMP.
تحميل Zip جاري التحميل... المهن ذات الصلة SOC
استنادا إلى تصنيف SOC المهني
name Contract Testing with Pact description Consumer-driven contract testing skill using Pact, covering consumer tests, provider verification, Pact Broker integration, and CI/CD workflows. version 1.0.0 author thetestingacademy license MIT tags ["pact","contract-testing","consumer-driven","api","microservices","provider-verification"] testingTypes ["contract","api"] frameworks ["pact"] languages ["typescript","java"] domains ["api"] agents ["claude-code","cursor","github-copilot","windsurf","codex","aider","continue","cline","zed","bolt"]
Contract Testing with Pact Skill
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.
Core Principles
Consumer-driven -- The consumer defines the contract; the provider must honor it.
Pact as contract -- Pact files are the single source of truth for API compatibility.
Independent deployability -- Contract tests ensure services can be deployed independently.
Broker as hub -- The Pact Broker mediates contract sharing and verification tracking.
Can-i-deploy -- Always verify deployment compatibility before deploying to production.
How Pact Works
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 │
└───────────────┘
Project Structure (TypeScript Consumer)
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
Project Structure (TypeScript Provider)
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
Consumer Test (TypeScript)
Setup
npm install --save-dev @pact-foundation/pact
Consumer API Client
axios, { } ;
{
: ;
: ;
: ;
: ;
}
{
: ;
: ;
: ;
}
{
: ;
( ) {
. = axios. ({ baseUrl });
}
( : ): < > {
response = . . ( );
response. ;
}
( : ): < > {
response = . . ( , data);
response. ;
}
( : = ): <{ : []; : }> {
response = . . ( );
response. ;
}
( : ): < > {
. . ( );
}
}
import
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
create
async
getUser
id
string
Promise
User
const
await
this
http
get
`/api/users/${id} `
return
data
async
createUser
data
CreateUserRequest
Promise
User
const
await
this
http
post
'/api/users'
return
data
async
listUsers
page
number
1
Promise
data
User
total
number
const
await
this
http
get
`/api/users?page=${page} `
return
data
async
deleteUser
id
string
Promise
void
await
this
http
delete
`/api/users/${id} `
Consumer Pact Test
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 () => {
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@]+$/ , 'user@example.com' ),
name : string ('John Doe' ),
role : regex (/^(admin|user|viewer)$/ , 'user' ),
},
});
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 : 'new@example.com' ,
name : 'New User' ,
password : 'SecurePass123!' ,
};
provider
.given ('the email new@example.com 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 ('new@example.com' ),
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 ('new@example.com' );
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 ('user@example.com' ),
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 );
});
});
});
});
Provider Verification (TypeScript)
import { Verifier } from '@pact-foundation/pact' ;
import path from 'path' ;
import { app } from '../../src/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' ,
pactUrls : [
path.resolve (process.cwd (), '../consumer-service/tests/pacts/frontend-app-user-service.json' ),
],
stateHandlers : {
'a user with ID user-123 exists' : async () => {
await db.users .create ({
id : 'user-123' ,
email : 'user@example.com' ,
name : 'John Doe' ,
role : 'user' ,
});
return { description : 'User user-123 created' };
},
'no user with ID nonexistent exists' : async () => {
await db.users .deleteMany ({ id : 'nonexistent' });
return { description : 'Ensured user does not exist' };
},
'the email new@example.com is not taken' : async () => {
await db.users .deleteMany ({ email : 'new@example.com' });
return { description : 'Email new@example.com is available' };
},
'there are users in the system' : async () => {
await db.users .createMany ({
data : [
{ id : 'user-1' , email : 'u1@example.com' , name : 'User 1' , role : 'user' },
{ id : 'user-2' , email : 'u2@example.com' , name : 'User 2' , role : 'admin' },
],
});
return { description : 'Seeded 2 users' };
},
},
publishVerificationResult : process.env .CI === 'true' ,
providerVersion : process.env .GIT_COMMIT || '1.0.0' ,
providerVersionBranch : process.env .GIT_BRANCH || 'main' ,
logLevel : 'warn' ,
});
await verifier.verifyProvider ();
});
});
Java Consumer (JUnit 5 + Pact) 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@]+$" , "user@example.com" )
.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();
}
}
Java Provider Verification 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" , "user@example.com" , "John Doe" , "user" ));
}
@State("no user with ID nonexistent exists")
void userDoesNotExist () {
userRepository.deleteById("nonexistent" );
}
@State("the email new@example.com is not taken")
void emailAvailable () {
userRepository.deleteByEmail("new@example.com" );
}
}
Pact Broker Integration
Publishing Pacts
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}
Can-I-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}
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}
Record 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}
CI/CD Integration
Consumer CI Pipeline 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 }}
Provider CI Pipeline name: Provider Contract Verification
on:
push:
branches: [main ]
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 }}
Pact Matchers Reference import { MatchersV3 } from '@pact-foundation/pact' ;
const {
like,
eachLike,
string ,
integer,
decimal,
boolean ,
uuid,
datetime,
date,
time,
regex,
fromProviderState,
arrayContaining,
atLeastOneLike,
} = MatchersV3 ;
const body = {
id : uuid (),
name : string ('John' ),
age : integer (30 ),
score : decimal (95.5 ),
active : boolean (true ),
email : regex (/^.+@.+\..+$/ , 'john@example.com' ),
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' ),
}),
};
Best Practices
Test the contract, not the implementation -- Pact tests verify the API shape, not business logic.
Use matchers, not exact values -- like() and regex() make contracts resilient.
Keep provider states minimal -- Just enough to make the interaction work.
Run can-i-deploy before every deployment -- It is your safety net against breaking changes.
Publish verification results from CI -- Not from local machines.
Use consumer version selectors -- Target mainBranch and deployedOrReleased pacts.
Tag versions in the broker -- Tag deployed versions to track what is in each environment.
Test error scenarios -- Include 404, 400, and 401 interactions.
Version pacts by git commit -- Use git rev-parse --short HEAD as the version.
Automate webhook triggers -- Configure the Pact Broker to trigger provider verification on new pacts.
Anti-Patterns to Avoid
Using Pact as an integration test -- Pact tests run against mocks, not real services.
Exact value matching everywhere -- Makes contracts brittle; use type matchers.
Testing every field combination -- One interaction per use case is enough.
Sharing pact files via email/chat -- Use a Pact Broker for proper lifecycle management.
Skipping provider states -- Without states, provider tests fail unpredictably.
Not publishing verification results -- The broker cannot track compatibility without results.
Ignoring can-i-deploy -- Deploying without checking compatibility causes outages.
Consumer testing provider internals -- Test only what the consumer actually uses.
One giant pact file -- Organize interactions by consumer feature/use case.
Not testing error responses -- Consumers must handle errors correctly.