contract-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseContract Testing
契约测试
Pact (Consumer-Driven Contracts)
Pact(消费者驱动契约)
Consumer Test (Node.js)
消费者侧测试(Node.js)
typescript
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike, string, integer } = MatchersV3;
const provider = new PactV3({
consumer: 'OrderService',
provider: 'ProductService',
});
describe('Product API contract', () => {
it('returns a product by ID', async () => {
await provider
.given('product 123 exists')
.uponReceiving('a request for product 123')
.withRequest({
method: 'GET',
path: '/api/products/123',
headers: { Accept: 'application/json' },
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: like({
id: string('123'),
name: string('Widget'),
price: integer(1999),
}),
})
.executeTest(async (mockserver) => {
const client = new ProductClient(mockserver.url);
const product = await client.getProduct('123');
expect(product.id).toBe('123');
expect(product.name).toBeDefined();
});
});
});typescript
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike, string, integer } = MatchersV3;
const provider = new PactV3({
consumer: 'OrderService',
provider: 'ProductService',
});
describe('Product API contract', () => {
it('returns a product by ID', async () => {
await provider
.given('product 123 exists')
.uponReceiving('a request for product 123')
.withRequest({
method: 'GET',
path: '/api/products/123',
headers: { Accept: 'application/json' },
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: like({
id: string('123'),
name: string('Widget'),
price: integer(1999),
}),
})
.executeTest(async (mockserver) => {
const client = new ProductClient(mockserver.url);
const product = await client.getProduct('123');
expect(product.id).toBe('123');
expect(product.name).toBeDefined();
});
});
});Provider Verification
提供者侧验证
typescript
import { Verifier } from '@pact-foundation/pact';
describe('Product provider verification', () => {
it('validates contracts', async () => {
await new Verifier({
providerBaseUrl: 'http://localhost:3000',
pactUrls: ['./pacts/OrderService-ProductService.json'],
// Or from broker:
// pactBrokerUrl: 'https://pact-broker.example.com',
// publishVerificationResult: true,
// providerVersion: process.env.GIT_SHA,
stateHandlers: {
'product 123 exists': async () => {
await db.product.create({ data: { id: '123', name: 'Widget', price: 1999 } });
},
},
}).verifyProvider();
});
});typescript
import { Verifier } from '@pact-foundation/pact';
describe('Product provider verification', () => {
it('validates contracts', async () => {
await new Verifier({
providerBaseUrl: 'http://localhost:3000',
pactUrls: ['./pacts/OrderService-ProductService.json'],
// Or from broker:
// pactBrokerUrl: 'https://pact-broker.example.com',
// publishVerificationResult: true,
// providerVersion: process.env.GIT_SHA,
stateHandlers: {
'product 123 exists': async () => {
await db.product.create({ data: { id: '123', name: 'Widget', price: 1999 } });
},
},
}).verifyProvider();
});
});Spring Cloud Contract
Spring Cloud Contract
Contract Definition (Groovy DSL)
契约定义(Groovy DSL)
groovy
// contracts/shouldReturnProduct.groovy
Contract.make {
description "should return product by ID"
request {
method GET()
url "/api/products/123"
headers { contentType applicationJson() }
}
response {
status OK()
headers { contentType applicationJson() }
body([
id: "123",
name: $(regex('[A-Za-z ]+')),
price: $(regex('[0-9]+')),
])
}
}groovy
// contracts/shouldReturnProduct.groovy
Contract.make {
description "should return product by ID"
request {
method GET()
url "/api/products/123"
headers { contentType applicationJson() }
}
response {
status OK()
headers { contentType applicationJson() }
body([
id: "123",
name: $(regex('[A-Za-z ]+')),
price: $(regex('[0-9]+')),
])
}
}Provider Base Test
提供者基础测试
java
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
public abstract class ContractBaseTest {
@Autowired MockMvc mockMvc;
@MockBean ProductRepository productRepo;
@BeforeEach
void setup() {
when(productRepo.findById("123"))
.thenReturn(Optional.of(new Product("123", "Widget", 1999)));
RestAssuredMockMvc.mockMvc(mockMvc);
}
}java
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
public abstract class ContractBaseTest {
@Autowired MockMvc mockMvc;
@MockBean ProductRepository productRepo;
@BeforeEach
void setup() {
when(productRepo.findById("123"))
.thenReturn(Optional.of(new Product("123", "Widget", 1999)));
RestAssuredMockMvc.mockMvc(mockMvc);
}
}Workflow
工作流程
1. Consumer writes contract test → generates pact file
2. Pact file published to broker (or shared via file)
3. Provider runs verification against pact
4. Both sides deploy only when contracts pass1. 消费者编写契约测试 → 生成pact文件
2. Pact文件发布到broker(或通过文件共享)
3. 提供者针对pact执行验证
4. 只有契约测试通过后双方才可部署Pact Broker
Pact Broker
bash
undefinedbash
undefinedPublish pact
Publish pact
npx pact-broker publish ./pacts
--consumer-app-version=$(git rev-parse HEAD)
--broker-base-url=https://pact-broker.example.com
--consumer-app-version=$(git rev-parse HEAD)
--broker-base-url=https://pact-broker.example.com
npx pact-broker publish ./pacts
--consumer-app-version=$(git rev-parse HEAD)
--broker-base-url=https://pact-broker.example.com
--consumer-app-version=$(git rev-parse HEAD)
--broker-base-url=https://pact-broker.example.com
Can I Deploy?
Can I Deploy?
npx pact-broker can-i-deploy
--pacticipant=OrderService
--version=$(git rev-parse HEAD)
--to-environment=production
--pacticipant=OrderService
--version=$(git rev-parse HEAD)
--to-environment=production
undefinednpx pact-broker can-i-deploy
--pacticipant=OrderService
--version=$(git rev-parse HEAD)
--to-environment=production
--pacticipant=OrderService
--version=$(git rev-parse HEAD)
--to-environment=production
undefinedAnti-Patterns
反模式
| Anti-Pattern | Fix |
|---|---|
| Testing implementation details | Test contract shape, not business logic |
| Over-specifying response fields | Use matchers (like, regex), not exact values |
| No state management | Define provider states for test data setup |
| Contracts not in CI | Run contract tests in CI/CD pipeline |
| Skipping provider verification | Both sides must run their tests |
| 反模式 | 修复方案 |
|---|---|
| 测试实现细节 | 测试契约结构,而非业务逻辑 |
| 过度指定响应字段 | 使用匹配器(like、regex),而非固定值 |
| 无状态管理 | 定义提供者状态用于测试数据准备 |
| 契约未接入CI | 在CI/CD流水线中运行契约测试 |
| 跳过提供者验证 | 双方都必须执行对应的测试 |
Production Checklist
上线检查清单
- Consumer-driven contracts for all API dependencies
- Pact Broker (or equivalent) for contract sharing
- Provider verification in CI pipeline
- check before deployment
can-i-deploy - Provider states for test data setup
- Contract versioned with git SHA
- 所有API依赖都实现了消费者驱动契约
- 用于契约共享的Pact Broker(或等效工具)
- CI流水线中包含提供者验证步骤
- 部署前执行检查
can-i-deploy - 配置了用于测试数据准备的提供者状态
- 契约使用git SHA进行版本管理