Claude CodeでPactのコントラクトテストを書き、API互換性をCIで守る
Claude CodeでPactのコンシューマテストとプロバイダ検証を書き、can-i-deployまでCIに載せる手順です。落ちた検証をClaudeに読ませて直す切り分けも載せます。
Pactのコントラクトテストは、APIを呼ぶ側(コンシューマ)が「このリクエストにこの形で返ってくるはず」という期待をテストとして書き、その期待をAPIを提供する側(プロバイダ)がCIで検証する仕組みです。Claude Codeに任せやすいのは、期待をテストに起こす作業と、検証が落ちたときの原因の切り分けです。契約ファイルそのものは生成物なので、Claudeに直接触らせない設計にします。
この記事ではJavaScript版のPactを例に、コンシューマのテスト、プロバイダの検証、CIへの組み込みの順に進めます。
Pactのコントラクトテストは何を守るのか
Pactは、HTTPやメッセージの連携を対象にしたコード先行(code-first)のコントラクトテストツールです。コントラクトテストは、アプリ同士がやり取りするメッセージが「共有された理解(契約)」に沿っているかを、アプリを単独で動かして確かめる手法です。結合テストのようにサービスを全部立ち上げる必要がありません。
Pactは「コンシューマ主導」です。契約は、コンシューマの自動テストを実行する過程で生成されます。この方式では、コンシューマが実際に使っている部分だけがテストされます。誰も使っていないレスポンスの項目は、プロバイダが自由に変えてもテストが落ちません。
OpenAPIのようなスキーマとは性格が違います。スキーマは「起こりうる全状態」を記述する静的な成果物です。Pactの契約は、具体的なリクエストとレスポンスの組をテストケースとして実行して強制する「例による契約」です。
OpenAPIとPactの守り方の違い
OpenAPI(スキーマ)
APIが取りうる形をすべて記述します。プロバイダのコードと文書が食い違っていないかを見るのに向きます。
Pact(契約)
コンシューマが実際に使うリクエストとレスポンスの組だけを検証します。使われていない項目は自由に変えられます。
プロバイダ単体でOpenAPI文書との一致を確かめるテストは、Pactのドキュメントでは「プロバイダのコントラクトテスト」と区別されています。それだけでは、コンシューマが正しく呼べているか、プロバイダが全コンシューマの期待を満たせるかは保証されません。OpenAPIの側を整える作業はClaude CodeでOpenAPI Generatorを使う手順や42CrunchプラグインでのOpenAPI監査で扱っています。Pactはその上に重ねる層です。
全体の流れと、Claudeに任せる範囲
PactのHTTP連携は次の順で進みます。
契約ができてCIで守られるまで
- 1
コンシューマのテストを書く
Pactが用意するモックのプロバイダを相手に、APIクライアントの動作を単体テストします。
- 2
契約ファイルが生成される
テストが通ると、やり取りがJSONの契約ファイルとして書き出されます。
- 3
契約をブローカーに公開する
Pact Brokerか、ファイル共有などの手段で、プロバイダ側に契約を渡します。
- 4
プロバイダが契約を再生して検証する
Pactが契約のリクエストを、ローカルで起動したプロバイダに投げて応答を比べます。
- 5
デプロイ前に can-i-deploy で確認する
検証結果の組み合わせ表を見て、そのバージョンを本番に出してよいかを判定します。
Claude Codeに任せる範囲は、1と4のテストコードの下書き、落ちた検証の原因の切り分け、CI設定の組み立てです。2の契約ファイルは触らせません。契約を変えたいときは、コンシューマのテストを直して再生成します。
コンシューマ側: 期待をテストに起こす
最初に、Claudeが守るべき約束を CLAUDE.md に書いておきます。Pactのドキュメントが挙げている注意点を、そのままプロジェクトの規約にする形です。
## Pact(コントラクトテスト)
- `pacts/` は生成物。直接編集しない。契約を変えるときは consumer 側のテストを直して再生成する
- コンシューマのテストでは、`fetchOrders` のような実際の API クライアント関数を呼ぶ。生の HTTP リクエストを直接投げない
- テスト実行前に `pacts/` を空にする。古いやり取りを残さない
- 正常系だけでなく 404 / 400 / 認可エラーの期待も、クライアントの振る舞いごとに書くテストで生のHTTPを投げてはいけない理由は、実際に動いているクライアントコードを通さないと、契約がコンシューマの実態からずれていくからです。クライアント関数を呼ぶ形にしておけば、クライアントを変えたときに契約も追随します。
そのうえで、Claudeには次のように頼みます。
claude -p "src/orderClient.js の fetchOrders に対する Pact のコンシューマテストを \
consumer/consumer.spec.js に書いて。CLAUDE.md の Pact の規約に従い、\
200 の正常系と 404 の期待を別々のインタラクションにする" \
--allowedTools "Read,Edit,Bash"claude -p は非対話モードで、--allowedTools に挙げたツールは確認なしで使えます。対話セッションで頼んでもかまいません。
Claudeが書くテストは、例えば次のような形になります。Pact公式の「5分ガイド」の注文APIの例に沿った書き方です。
const { PactV3, MatchersV3 } = require("@pact-foundation/pact");
const path = require("path");
const { fetchOrders } = require("../src/orderClient");
const provider = new PactV3({
consumer: "OrderWeb",
provider: "OrderApi",
dir: path.resolve(process.cwd(), "pacts"),
logLevel: "info",
});
describe("OrderApi との契約", () => {
it("注文一覧を受け取れる", () => {
provider
.given("there are orders")
.uponReceiving("a request for orders")
.withRequest({ method: "GET", path: "/orders" })
.willRespondWith({
status: 200,
headers: { "Content-Type": "application/json; charset=utf-8" },
body: MatchersV3.eachLike({
id: 1,
items: MatchersV3.eachLike({ name: "burger", quantity: 2, value: 100 }),
}),
});
return provider.executeTest(async (mockserver) => {
process.env.API_PORT = mockserver.port;
const orders = await fetchOrders();
expect(orders[0].id).toBe(1);
});
});
});読むべき点は3つあります。
givenが「プロバイダにこのデータがある」という前提(プロバイダステート)の名前になる。givenとuponReceivingの組み合わせは契約内で一意にするeachLikeで「配列の各要素がこの形」と書く。値を固定するのではなく型と構造を契約にするexecuteTestに渡す関数の中で、モックサーバーのポートをクライアントに渡して実際の関数を呼ぶ
「5分ガイド」は、正常系のほかに404、400、500、401・403を考えるよう案内しています。Claudeに頼むときも、ステータスごとにクライアントがどう振る舞うか(nullを返すのか、例外にするのか)を指示に含めておくと、期待が抽象的にならずに済みます。500は、プロバイダに意図的にエラーを起こさせる状態を作りにくいため、Pactを使わない通常の単体テストにする選択肢も同じガイドが示しています。
Claudeの編集範囲を機械的に絞るなら、.claude/settings.json にdenyルールを置きます。
{
"permissions": {
"deny": ["Edit(pacts/**)"]
}
}Edit(パス) のdenyルールはファイル編集ツールの操作を止めます。契約の中身を直接直されるのを防げます。契約ファイルはテスト実行のたびに書き出されるため、仮に手で直してもその場しのぎにしかなりません。
プロバイダ側: 契約を再生して検証する
プロバイダ側は、契約を取得して、ローカルで起動したAPIに再生するテストを書きます。Pact JSでは Verifier を使います。次はブローカーから契約を取る構成の例です(オプション名はPact JSのプロバイダ検証のドキュメントに沿っています)。
const { Verifier } = require("@pact-foundation/pact");
const opts = {
provider: "OrderApi",
providerBaseUrl: `http://localhost:${port}`,
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
providerVersion: process.env.GIT_COMMIT,
providerVersionBranch: process.env.GIT_BRANCH,
publishVerificationResult: process.env.CI === "true",
consumerVersionSelectors: [
{ mainBranch: true },
{ deployedOrReleased: true },
],
stateHandlers: {
"there are orders": () => {
importData();
return Promise.resolve({ description: "orders added" });
},
},
};
new Verifier(opts).verifyProvider();検証結果をブローカーに送る publishVerificationResult は、CIのビルドでだけ有効にします。ドキュメントも、有効にするのはCIのビルド中だけにするよう案内しています。
stateHandlers は、契約の given に書かれた状態ごとに、プロバイダのデータを用意する関数です。この設定が無いと、Verifierは契約に書かれたプロバイダステートを無視して警告を出すだけになります。データを用意する関数が無いと、前提のデータが無いまま応答を比べることになります。
プロバイダのテストは、ドキュメントの方針に従って「サービス全体を動かし、データベースや外部サービスはモックにする」構成が基本です。結合テストに近い確かさを得ながら、保守コストを抑えられます。
落とし穴
新しい契約を足した途端にプロバイダのビルドが落ちる
コンシューマが新しい契約や変更を公開すると、プロバイダの検証がまだ通らず、プロバイダのビルドが赤くなることがあります。Pactには「Pending pacts」という機能があり、検証していない契約を最初は「保留」扱いにして、結果は返しつつプロバイダのビルド自体は落とさないようにできます。Verifierに enablePending: true を渡して使います。PactFlowでは標準で使えますが、自前のブローカーでは設定が要ります。
CIに載せる: 公開、検証、can-i-deploy
CIでは3つの仕事が回ります。コンシューマ側で契約を公開し、プロバイダ側で検証して結果を返し、デプロイ前に can-i-deploy で判定します。
コンシューマ側は、テストのあとに契約をブローカーへ公開します。Pact JSのドキュメントは、@pact-foundation/pact-cli を入れて pact-broker publish を使う形を示しています。認証情報は PACT_BROKER_TOKEN(または PACT_BROKER_USERNAME と PACT_BROKER_PASSWORD)の環境変数で渡せます。
name: consumer-contract
on:
pull_request:
push:
branches: [main]
jobs:
contract:
runs-on: ubuntu-latest
env:
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
steps:
- uses: actions/checkout@v6
- run: npm ci
- run: rm -rf pacts && npm run test:consumer
- run: |
npx pact-broker publish pacts \
--consumer-app-version="${{ github.sha }}" \
--auto-detect-version-properties \
--broker-base-url="${{ vars.PACT_BROKER_BASE_URL }}"
- run: |
npx pact-broker can-i-deploy \
--pacticipant OrderWeb \
--version "${{ github.sha }}" \
--to-environment productionrm -rf pacts を毎回入れているのは、古いインタラクションが契約に残らないようにするためです。最後の can-i-deploy は、そのバージョンが本番にいるプロバイダのバージョンと検証済みの組み合わせかを判定します。互換なら終了コード0、そうでなければ1で、そのままデプロイの関門にできます。
判定の元になるのは、コンシューマとプロバイダのバージョンの組ごとに検証が通ったかを並べた「マトリクス」です。これが成り立つよう、デプロイ後に pact-broker record-deployment で「このバージョンがこの環境にいる」とブローカーへ知らせます。can-i-deploy には、検証結果の到着を待つポーリングや、結果を見るだけで終了コードを常に成功にするdry runモードもあります。最初はdry runで流れを確かめてから、関門として有効にするのが安全です。
プロバイダ側のCIは、前節のVerifierを実行するジョブを置くだけです。プロバイダがAPIを変更するプルリクエストで、この検証が落ちる状態を作るのが目的です。
落ちた検証をClaudeに読ませて直す
検証が落ちたとき、最初に決めるのは「どちらが間違っているか」です。Claudeに読ませる前に、原因の型を頭に入れておくと指示が具体的になります。
検証失敗の3つの原因
プロバイダの退行
契約どおりの応答が返らなくなった場合です。コンシューマは使っている項目が消えて困ります。直すのはプロバイダのコードです。
コンシューマの期待が過剰
実際には使わない項目や、固定値にしなくてよい値まで契約に入れていた場合です。直すのはコンシューマのテストで、契約を再生成します。
プロバイダステートの未設定
givenの状態に対応するstateHandlersが無く、データが無いまま応答を比べて落ちた場合です。直すのはプロバイダ側の検証コードです。
ローカルでの依頼は次のようにします。
npm run test:provider 2>&1 | tail -n 60 > verify.log
claude -p "verify.log は Pact のプロバイダ検証の失敗ログ。\
どのインタラクションが、何の差分で落ちたかを要約し、\
上の 3 つの原因のどれに当たるかを根拠付きで答えて。\
まだコードは直さないで" --allowedTools "Read"原因の見立てを人が確認してから、修正を依頼します。この順を踏むのは、プロバイダの退行を「契約側を緩めて」解消してしまう危険があるからです。落ちたテストを通すことだけを目的にすると、コンシューマの期待を弱める方向の修正が入りうるので、触ってよい範囲を指示で固定します。「プロバイダのコードを直す。契約(pacts/)とコンシューマのテストは変えない」と明示します。
PR上で直す場合は、Claude CodeのGitHub Actionsを使います。@claude のメンションに反応する構成では、権限に actions: read を付けると、ClaudeがPRのCI結果を読めます。
@claude プロバイダ検証 (provider-verify ジョブ) が落ちています。
ログを読み、プロバイダのコードを直してください。
pacts/ とコンシューマのテストは変更しないでください。直した結果のpushでプロバイダ検証が再実行されるかは、トークンの種類に左右されます。Claude CodeのGitHub Actionのトラブルシューティングによれば、既定の GITHUB_TOKEN で作ったコミットではGitHubがワークフローを起動しません。github_token: ${{ secrets.GITHUB_TOKEN }} をアクションに渡しているなら外して、ClaudeのGitHub Appとして認証させるか、独自のアプリトークンを使います。CI側のワークフローが push や pull_request で起動する設定かも見ます。ここを外すと、Claudeが直しても検証が走らず、赤いままに見えます。
Pactが向かない場面
PactのFAQには、向かないケースが明記されています。Claudeに書かせる前に、題材が合っているかを確かめます。
- プロバイダがリクエストの中身を検証せず、下流のシステムへそのまま渡すだけのAPIは、Pactに向きません。Pactの検証が見るのはレスポンス本文の一致で、リクエストが引き起こす副作用(キューへの投入など)は見ません
- 公開APIをテストする場合、プロバイダステートを用意する手段が、テスト対象のAPI自身になります。通常の検証よりも遅く壊れやすくなります
- 単体のサービスと単体のクライアントの組でも使えますが、効果が大きいのは多数のサービスが連携するマイクロサービス構成です
契約を書くのが誰かという点も確かめます。コンシューマ側のチームがテストを書き、プロバイダ側のチームが検証をCIに載せる、という分担が、FAQの前提です。
まとめ
Claude Codeに任せて効くのは、クライアント関数を通すコンシューマテストの下書き、失敗ログの切り分け、CI設定の組み立ての3つです。契約ファイルは再生成物として扱い、CLAUDE.md とdenyルールで直接編集を止めます。
落ちた検証を直すときは、プロバイダを直すのか、コンシューマの期待を見直すのかを先に決めます。その判断を人が握り、Claudeには「どこを触らないか」まで含めて頼むと、テストを通すために契約が緩められる事態を避けられます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。