--- id: testing-contract-testing title: Contract Testing — 서비스 간 약속 검증 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [testing, contract-test, pact, microservices, vibe-coding] tech_stack: { language: "TypeScript / Pact", applicable_to: ["Backend microservices"] } applied_in: [] aliases: [consumer-driven contract, Pact, schema test] --- # Contract Testing > 마이크로서비스 환경에서 E2E 테스트는 비싸고 느리다. **각 서비스가 "다른 서비스와의 약속(contract)" 만 검증** = consumer-driven contract. 약속 깨면 즉시 파이프라인 fail. ## 📖 핵심 개념 - Consumer 가 "이 형식의 응답을 기대한다" 라는 contract 작성. - Provider 는 그 contract 를 받아 자기 코드가 약속을 지키는지 검증. - 양쪽이 contract 를 통해 통신, E2E 환경 안 띄움. ## 💻 코드 패턴 (Pact) ### Consumer 측 (Frontend / 다른 서비스) ```ts import { PactV3, MatchersV3 } from '@pact-foundation/pact'; const { like, eachLike, integer } = MatchersV3; const provider = new PactV3({ consumer: 'web-app', provider: 'user-service', port: 1234 }); test('GET /users/:id', async () => { await provider .given('user 1 exists') .uponReceiving('a request for user 1') .withRequest({ method: 'GET', path: '/users/1' }) .willRespondWith({ status: 200, headers: { 'Content-Type': 'application/json' }, body: like({ id: integer(1), email: like('a@a.com'), roles: eachLike('admin'), }), }); await provider.executeTest(async (mock) => { const res = await fetch(`${mock.url}/users/1`).then(r => r.json()); expect(res.id).toBe(1); }); }); ``` 실행 결과 = `pacts/web-app-user-service.json` 파일. ### Provider 측 검증 ```ts import { Verifier } from '@pact-foundation/pact'; test('user-service satisfies web-app contract', async () => { const v = new Verifier({ provider: 'user-service', providerBaseUrl: 'http://localhost:3000', pactUrls: ['./pacts/web-app-user-service.json'], // 또는 Pact Broker stateHandlers: { 'user 1 exists': async () => { await db.users.upsert({ id: 1, email: 'a@a.com' }); }, }, }); await v.verifyProvider(); }); ``` ### Pact Broker — 중앙 저장 - consumer CI: pact 파일 publish. - provider CI: 최신 pact 받아 검증. - can-i-deploy 명령으로 안전한 deploy 확인. ## 🤔 의사결정 기준 | 상황 | 권장 | |---|---| | 마이크로서비스 다수 (>3개) | Pact 도입 | | 단일 서비스 + DB | unit + integration 만 충분 | | Public API (외부 사용자) | OpenAPI 스키마 + 검증 | | GraphQL | schema introspection + breaking change detector | | gRPC | proto 가 자체 contract | | Frontend ↔ Backend | Pact 또는 OpenAPI generator (zod, ts-rest) | ## ❌ 안티패턴 - **contract 안에 정확한 값 (literal) 만 명시**: 실 응답이 약간만 달라도 fail. matcher 사용 (like / eachLike / regex). - **provider state handler 없음**: contract 가 "user 1 exists" 가정 — handler 없으면 검증 못 함. - **breaking change 없이 contract 갱신**: 옛 consumer 가 깨짐. semantic versioning + can-i-deploy. - **pact 만 있고 unit/integration test 없음**: pact 는 약속만, 비즈니스 로직 안 검증. - **양쪽 모두 mock**: contract 가 양쪽에서 거짓말이면 의미 없음. provider 는 진짜 코드 검증. - **contract test 가 가짜 데이터 fixture 의존**: stateHandlers 로 진짜 상태 만들어야. ## 🤖 LLM 활용 힌트 - consumer 측: matcher 사용 (literal X). - provider 측: stateHandler 로 진짜 DB 상태. - Pact Broker + can-i-deploy 가 deploy gate. ## 🔗 관련 문서 - [[Testing_Test_Pyramid]] - [[Testing_Mocking_Boundaries]]