--- 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]]