[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
---
|
||||
id: testing-pact-contract-deep
|
||||
title: Pact Contract Testing — consumer-driven contracts
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [testing, contract, vibe-coding]
|
||||
tech_stack: { language: "TS / Java", applicable_to: ["Backend", "Testing"] }
|
||||
applied_in: []
|
||||
aliases: [Pact, contract testing, consumer driven, schema test, OpenAPI test, broker]
|
||||
---
|
||||
|
||||
# Pact Contract Testing
|
||||
|
||||
> Microservice / API consumer-provider 의 schema mismatch = production 발견. **Pact: consumer 가 contract 정의 → broker → provider verify**. E2E 보다 빠름 + 안정.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Consumer 가 expected interaction 정의.
|
||||
- Provider 가 verify (모든 expectation 처리).
|
||||
- Broker = central registry (Pactflow / OSS).
|
||||
- Bi-directional (OpenAPI 비교 도).
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Consumer test (TS)
|
||||
```ts
|
||||
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
|
||||
const { like, integer } = MatchersV3;
|
||||
|
||||
const provider = new PactV3({
|
||||
consumer: 'web-app',
|
||||
provider: 'user-api',
|
||||
});
|
||||
|
||||
provider
|
||||
.given('user 1 exists')
|
||||
.uponReceiving('a request for user 1')
|
||||
.withRequest({ method: 'GET', path: '/users/1' })
|
||||
.willRespondWith({
|
||||
status: 200,
|
||||
body: like({ id: integer(1), name: 'Alice' }),
|
||||
});
|
||||
|
||||
await provider.executeTest(async (mock) => {
|
||||
const r = await fetch(`${mock.url}/users/1`);
|
||||
expect(r.status).toBe(200);
|
||||
});
|
||||
// → pact JSON 생성
|
||||
```
|
||||
|
||||
### Pact JSON
|
||||
```json
|
||||
{
|
||||
"consumer": { "name": "web-app" },
|
||||
"provider": { "name": "user-api" },
|
||||
"interactions": [{
|
||||
"providerStates": [{ "name": "user 1 exists" }],
|
||||
"request": { "method": "GET", "path": "/users/1" },
|
||||
"response": { "status": 200, "body": { "id": 1, "name": "Alice" } }
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
→ Broker 에 publish.
|
||||
|
||||
### Provider verify
|
||||
```ts
|
||||
import { Verifier } from '@pact-foundation/pact';
|
||||
|
||||
await new Verifier({
|
||||
providerBaseUrl: 'http://localhost:3000',
|
||||
pactBrokerUrl: 'https://broker.example.com',
|
||||
provider: 'user-api',
|
||||
publishVerificationResult: true,
|
||||
providerVersion: '1.2.3',
|
||||
stateHandlers: {
|
||||
'user 1 exists': async () => {
|
||||
await db.users.insert({ id: 1, name: 'Alice' });
|
||||
},
|
||||
},
|
||||
}).verifyProvider();
|
||||
```
|
||||
|
||||
→ 매 interaction = stateHandler setup → 호출 → 응답 검증.
|
||||
|
||||
### Broker
|
||||
```bash
|
||||
docker run -p 9292:9292 pactfoundation/pact-broker
|
||||
|
||||
# Publish
|
||||
pact-broker publish ./pacts \
|
||||
--consumer-app-version 1.0.0 \
|
||||
--broker-base-url https://broker
|
||||
|
||||
# Can-i-deploy
|
||||
pact-broker can-i-deploy \
|
||||
--pacticipant web-app --version 1.0.0 \
|
||||
--to production
|
||||
```
|
||||
|
||||
→ Broker 가 매 version compatibility check.
|
||||
|
||||
### Matchers
|
||||
```ts
|
||||
import { MatchersV3 } from '@pact-foundation/pact';
|
||||
const { like, term, eachLike, integer, decimal, datetime, uuid } = MatchersV3;
|
||||
|
||||
body: {
|
||||
id: integer(1), // any int
|
||||
email: term({ generate: 'a@x.com', matcher: '\\S+@\\S+' }), // regex
|
||||
age: like(25), // type
|
||||
items: eachLike({ name: 'book' }, { min: 1 }), // array of like
|
||||
created: datetime("yyyy-MM-dd'T'HH:mm:ss"),
|
||||
uuid: uuid(),
|
||||
}
|
||||
```
|
||||
|
||||
→ Schema 매칭. Concrete value X.
|
||||
|
||||
### Pact vs OpenAPI
|
||||
```
|
||||
Pact:
|
||||
- Consumer-driven (실제 사용).
|
||||
- Interaction 별 (state + request + response).
|
||||
- Provider 가 verify (running app).
|
||||
|
||||
OpenAPI:
|
||||
- Provider-published (모든 가능 endpoint).
|
||||
- Schema 만.
|
||||
- Static check.
|
||||
|
||||
→ OpenAPI = "이거 가 가능". Pact = "이거 가 사용".
|
||||
```
|
||||
|
||||
→ 둘 다 가능. Bi-directional pact (OpenAPI ↔ Pact).
|
||||
|
||||
### Bi-directional contract
|
||||
```
|
||||
Provider publishes OpenAPI spec.
|
||||
Consumer publishes Pact.
|
||||
Broker compares.
|
||||
|
||||
→ Provider 가 actual run 안 — schema 만 검증.
|
||||
```
|
||||
|
||||
### Async / Kafka
|
||||
```ts
|
||||
// Producer (의 message)
|
||||
.uponReceiving('an order created event')
|
||||
.withContent({
|
||||
orderId: integer(123),
|
||||
userId: integer(1),
|
||||
total: decimal(99.99),
|
||||
});
|
||||
|
||||
// Consumer 가 verify (read message → assert)
|
||||
```
|
||||
|
||||
### Versioning
|
||||
```
|
||||
Consumer publishes pact-v1, pact-v2.
|
||||
Provider verifies 둘 다.
|
||||
|
||||
→ 옛 client 도 작동 가 검증.
|
||||
```
|
||||
|
||||
### CI integration
|
||||
```yaml
|
||||
# Consumer
|
||||
- run: yarn test:pact
|
||||
- run: pact-broker publish pacts --consumer-app-version $GIT_SHA
|
||||
|
||||
# Provider (PR + merge)
|
||||
- run: yarn test:provider-verify
|
||||
- run: pact-broker can-i-deploy --pacticipant user-api --version $GIT_SHA --to production
|
||||
```
|
||||
|
||||
→ 매 PR 가 broker check.
|
||||
|
||||
### `can-i-deploy`
|
||||
```bash
|
||||
pact-broker can-i-deploy \
|
||||
--pacticipant web-app --version 2.0.0 \
|
||||
--to production
|
||||
|
||||
# Computer says no
|
||||
# - web-app 2.0.0 의 pact 가 user-api 1.5.0 에서 verified X
|
||||
```
|
||||
|
||||
→ Deploy 막음 — schema mismatch 가 prod 안 감.
|
||||
|
||||
### Webhook
|
||||
```bash
|
||||
# Consumer 가 새 pact publish → broker 가 provider CI trigger.
|
||||
pact-broker create-webhook \
|
||||
--consumer web-app --provider user-api \
|
||||
--request POST --url https://ci.example.com/build \
|
||||
--description "trigger provider verify"
|
||||
```
|
||||
|
||||
### State handler
|
||||
```ts
|
||||
stateHandlers: {
|
||||
'user exists': async (params) => {
|
||||
await db.users.insert({ id: params.id, name: params.name });
|
||||
return { id: params.id };
|
||||
},
|
||||
'user does not exist': async () => {
|
||||
await db.users.deleteAll();
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
→ Provider state setup. Test isolation.
|
||||
|
||||
### Polyglot (다른 언어)
|
||||
```
|
||||
Pact = 다 언어 (Java, Ruby, Go, Python, JS, C#).
|
||||
JSON spec 표준.
|
||||
|
||||
→ Consumer JS, Provider Java OK.
|
||||
```
|
||||
|
||||
### When NOT contract test?
|
||||
```
|
||||
- Monolith (1 deploy)
|
||||
- 1 client + 1 server (직접 test)
|
||||
- API 가 매우 stable
|
||||
- 변경 자주 (overhead)
|
||||
|
||||
→ Microservice (여러 client, 여러 provider, 자주 deploy) 가 sweet spot.
|
||||
```
|
||||
|
||||
### Cost / overhead
|
||||
```
|
||||
- Pact infrastructure (broker, CI)
|
||||
- Test 작성 cost
|
||||
- 매 변경 = consumer + provider 협조
|
||||
|
||||
→ 5+ service team 가 가치.
|
||||
```
|
||||
|
||||
### Storybook + MSW + Pact
|
||||
```ts
|
||||
// Storybook story 가 MSW handler 사용
|
||||
// MSW handler 가 Pact 와 align
|
||||
// → UI test 가 contract align
|
||||
```
|
||||
|
||||
→ Frontend 도 contract.
|
||||
|
||||
### SchemaThesis (alternative)
|
||||
```bash
|
||||
# OpenAPI 기반 fuzz test
|
||||
schemathesis run https://api.example.com/openapi.json
|
||||
```
|
||||
|
||||
→ OpenAPI 의 모든 endpoint 가 fuzz.
|
||||
|
||||
### Function vs API
|
||||
```
|
||||
Pact: HTTP API.
|
||||
TypeScript: type-level (compile-time check).
|
||||
gRPC: proto buf.
|
||||
|
||||
→ Pact 가 HTTP / queue 친화.
|
||||
```
|
||||
|
||||
### 함정
|
||||
```
|
||||
- 모든 interaction pact: 큰 file, 느린 verify.
|
||||
- Specific value (가짜 ID 1): brittle. matchers.
|
||||
- State 쟁이 함: setup 어려움.
|
||||
- Pact 가 functional / business test 됨: scope creep.
|
||||
- Provider 가 pact 무시: silent break.
|
||||
- can-i-deploy 사용 X: pact 의 가치 ↓.
|
||||
```
|
||||
|
||||
### Best practices
|
||||
```
|
||||
1. Consumer 가 minimum interaction (실제 필요한 것만).
|
||||
2. Matchers > 정확 value.
|
||||
3. State handler 가 idempotent.
|
||||
4. CI 가 매 PR 검증.
|
||||
5. can-i-deploy 가 prod gate.
|
||||
6. Broker 의 tag (env, branch).
|
||||
```
|
||||
|
||||
### Example workflow
|
||||
```
|
||||
1. Consumer dev: write code → test (pact).
|
||||
2. Pact publish to broker.
|
||||
3. Webhook trigger provider CI.
|
||||
4. Provider verify pact.
|
||||
5. Result publish to broker.
|
||||
6. Consumer can deploy if all green.
|
||||
7. Provider can deploy if all consumer pact green.
|
||||
```
|
||||
|
||||
→ Bi-directional gate.
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| 5+ microservice | Pact |
|
||||
| API 자주 변경 | Pact |
|
||||
| OpenAPI 만 | Bi-directional |
|
||||
| Async / Kafka | Pact message |
|
||||
| 작은 system | OpenAPI + integration test |
|
||||
| Monolith | E2E 만 |
|
||||
| API 가 stable | Schema test 만 |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **모든 interaction**: pact 폭발.
|
||||
- **State handler 가 unhealthy**: test 불안정.
|
||||
- **Provider 가 verify 안 함**: silent break.
|
||||
- **can-i-deploy 무시**: pact 의 가치 ↓.
|
||||
- **정확 value (no matchers)**: brittle.
|
||||
- **Pact 가 business test**: scope creep.
|
||||
- **Broker 백업 X**: history 잃음.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- Consumer-driven > provider-defined.
|
||||
- Matchers (like, term) 가 핵심.
|
||||
- can-i-deploy 가 prod gate.
|
||||
- Pactflow (managed) 가 작은 팀 친화.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Testing_Contract_Testing]]
|
||||
- [[API_OpenAPI_Spec]]
|
||||
- [[Backend_GraphQL_Server_Patterns]]
|
||||
Reference in New Issue
Block a user