Files
2nd/10_Wiki/Topics/Coding/Testing_Pact_Contract_Deep.md
T
2026-05-10 22:08:15 +09:00

7.8 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
testing-pact-contract-deep Pact Contract Testing — consumer-driven contracts Coding draft B conceptual 2026-05-09 2026-05-09
testing
contract
vibe-coding
language applicable_to
TS / Java
Backend
Testing
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)

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

{
  "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

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

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

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

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

# 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

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

# 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

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

// Storybook story 가 MSW handler 사용
// MSW handler 가 Pact 와 align
// → UI test 가 contract align

→ Frontend 도 contract.

SchemaThesis (alternative)

# 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) 가 작은 팀 친화.

🔗 관련 문서