| name | levels-service |
| description | テストの粒度選択のうち、サービスを独立にデプロイ可能な箱とみなすサービス間の層 (コンポーネントテスト、コントラクトテスト/Consumer-Driven Contract/Pact、 API スキーマ検証)を扱う。 test-catalog の手法カタログの一部。外部依存だけスタブ化した箱単体の振る舞い検証、 消費側駆動の契約テストによるサービス間互換性の保証、OpenAPI/JSON Schema への構造 準拠を検証したい、または割り当てたいときに使う。通常は test-catalog スキルの索引 経由で手法が選定された後にこのスキルを直接参照する。
|
| disable-model-invocation | true |
テストレベル(サービス間: コンポーネント・コントラクト)
サービスを独立にデプロイ可能な箱とみなし、その箱単体の振る舞いと、サービス間 API の契約を固めるレベルを扱う。
サービス内部の粒度(単体、結合)は levels.md、システム全体を外から見るレベルは levels-system.md / levels-operational.md を参照。
各レベルは「概要 / 目的といつ使うか / TypeScript example / 落とし穴 / 網羅の定義 / 遂行手順(着手→完了)/ 完了チェック(もれ確認)」で示す。
目次
コンポーネントテスト
概要
1 つのサービスやモジュール群を 1 単位として、外部依存だけスタブ化して内部は本物のまま検証する。
目的/いつ使う
あるサービスを独立にデプロイ可能な箱とみなし、その箱単体の振る舞いを契約として固めたいときに使う。
複数サービスの連携全体を見たい局面では使わない(それはシステムテスト)。
TypeScript example
import { describe, it, expect, vi } from "vitest";
import { OrderService } from "./order-service";
describe("OrderService(コンポーネント)", () => {
it("在庫があれば注文を確定する", async () => {
const payment = { charge: vi.fn().mockResolvedValue({ ok: true }) };
const svc = new OrderService(payment);
const result = await svc.placeOrder({ sku: "A1", qty: 2 });
expect(result.status).toBe("confirmed");
expect(payment.charge).toHaveBeenCalledOnce();
});
});
落とし穴
スタブの返す形が実物の API とずれると、緑のまま本番で壊れる。
ここをコントラクトテストで裏打ちしないと、コンポーネント単体の安心は錯覚になる。
網羅の定義
このレベルは「1 サービスを箱とみなした外部契約としての振る舞い」を網羅対象に取る層。
外部依存はスタブ化し、内部は本物のままサービスの契約を踏む。
- 網羅基準(いつ網羅完了とみなすか):サービスが外部へ公開する振る舞い(主要なユースケースとその代表的な失敗)を、内部を本物にした状態で一通り踏んだとき。内部実装の分岐網羅は基準に含めない(単体の責務)。
- 網羅手順(基準を満たすケース集合の作り方):1. サービスの公開操作(エンドポイント/コマンド)を列挙する。2. 各操作の正常系と業務上意味のある失敗系を代表ケースにする。3. 外部依存をスタブ化し、内部ロジックは本物のまま結線して検証する。
- 達成チェック(漏れの検出):スタブの返す形が実 API とずれていないか。そのずれをコントラクトテストで裏打ちしているか(裏打ちが無いと緑のまま本番で壊れる)。
遂行手順(着手→完了)
コンポーネントの作業の本体は「サービスが外へ公開する操作を列挙し、外部依存だけスタブ化して内部は本物のまま、各操作の代表を踏む」ことだ。完了判定は「全公開操作 × {正常, 意味のある失敗} を踏んだか」で測る。
- 公開操作を列挙する:サービスが外部へ晒すエンドポイント/コマンド/メッセージハンドラを全部書き出す。1操作が網羅の単位。内部メソッドは対象にしない(操作経由で踏む)。
- 外部依存を洗い出してスタブ境界を引く:サービスが呼ぶプロセス外依存(決済 API・他サービス・外部キュー)を列挙し、そこだけをスタブ化対象にする。内部のドメインロジックやサービス内 DB は本物のまま結線する(
test-doubles.md の振り分け)。
- スタブを実物の契約に整合させる:各スタブの戻り形を、実 API の必須フィールド・型・エラー表現に合わせる。整合の根拠を コントラクトテスト(pact)に置き、勝手な形を返させない。
- 各操作の代表ケースを書く:操作ごとに正常系1本 + 業務上意味のある失敗系1本(在庫切れ・決済拒否など)。内部実装の分岐網羅はここでやらない(単体の責務)。
- 完了本数を固定する:完了本数 = 公開操作数 × 代表本数。手順1の操作一覧から逆算し、満たしたら打ち切る。
完了チェック(もれ確認)
- 全公開操作にケースが当たったか:手順1で挙げた操作の数と、ケースの付いた操作の数が一致する。差があれば操作の取りこぼし。
- 各操作が正常+失敗を持つか:操作ごとに正常系と代表失敗系が揃っているか。
- スタブ境界が外部依存だけか:スタブ化したものが手順2の「プロセス外依存」だけか。サービス内 DB やドメインロジックまでスタブ化していたらコンポーネントでなく単体に縮退している。
- スタブ形が契約で裏打ちされているか:各スタブの戻り形に対応する pact があるか。裏打ちの無いスタブは「緑のまま本番で壊れる」穴。
- 内部分岐が紛れていないか:1操作に多数のケースが付いていたら、内部分岐網羅が漏れた疑い。単体へ差し戻す。
コントラクトテスト(Consumer-Driven Contract / Pact)
概要
サービス間の API 契約(リクエスト/レスポンスの形)を、消費側が定義し提供側が満たすことを両端で検証する。
目的/いつ使う
マイクロサービスやチーム分割で、相手を立ち上げずに連携の互換性を担保したいときに使う。
単一プロセス内の呼び出しや、めったに変わらない安定 API には過剰。
TypeScript example
import { PactV3, MatchersV3 } from "@pact-foundation/pact";
import { describe, it, expect } from "vitest";
import path from "node:path";
import { fetchUser } from "./user-client";
const { like } = MatchersV3;
describe("user-client の契約", () => {
it("GET /users/:id がユーザーを返す", async () => {
const provider = new PactV3({
consumer: "web",
provider: "user-api",
dir: path.resolve(process.cwd(), "pacts"),
});
provider
.uponReceiving("a request for user 1")
.withRequest({ method: "GET", path: "/users/1" })
.willRespondWith({
status: 200,
body: { id: like(1), name: like("alice") },
});
await provider.executeTest(async (mock) => {
const user = await fetchUser(mock.url, 1);
expect(user.name).toBe("alice");
});
});
});
落とし穴
生成した pact を提供側の CI で検証しないと、契約はただのモック設定に堕ちる。
like で型だけ緩く合わせると、必須フィールドの欠落を見逃す。重要な値は具体例で固定する。
網羅の定義
このレベルは「サービス間 API の契約」を網羅対象に取る層。
消費側が実際に必要とするリクエスト/レスポンスの形を、両端で踏む。
- 網羅基準(いつ網羅完了とみなすか):消費側が利用する全エンドポイント × 期待するレスポンス形(必須フィールドを含む)を pact 化し、それを提供側 CI が検証して緑になったとき。
- 網羅手順(基準を満たすケース集合の作り方):1. 消費側コードが呼ぶエンドポイントを全て洗い出す。2. 各エンドポイントで消費側が依存する必須フィールドと値を具体例で固定する。3. 生成した pact を提供側の CI に渡して検証ジョブに組み込む。
- 達成チェック(漏れの検出):提供側で pact 検証が実際に回っているか(回らなければただのモック設定)。
like で緩めすぎて必須フィールドの欠落を見逃していないか。消費側に未カバーの呼び出しが残っていないか。
遂行手順(着手→完了)
コントラクトの作業の本体は「消費側が実際に依存する期待を列挙して pact 化し、提供側 CI でそれを検証し、互換マトリクスで両端の整合を確認する」ことだ。完了は「消費側の全呼び出しが pact 化され、提供側検証が緑」で測る。
- 消費側の期待を列挙する:消費側コードが実際に呼ぶエンドポイント × 依存する必須フィールド/値を全部書き出す。使っていないフィールドは契約に入れない(消費側駆動=使う分だけ固める)。
- 期待を pact で固定する:各エンドポイントの必須フィールドは
like の型緩和でなく具体例で固定する。値の有無で分岐する応答(404・権限エラー等)は別 interaction として立てる。
- provider 側で pact を検証する:生成 pact を提供側 CI の検証ジョブに渡し、提供側の実レスポンスが pact を満たすことを確認する。検証ジョブが無ければ pact はただのモック設定。
- 互換マトリクスを作る:
(consumer バージョン × provider バージョン) の表で、どの組合せの pact が検証緑かを管理する(pact broker の can-i-deploy 等)。デプロイ前にこの表で互換を確認する。
- 未カバーを差し戻す:手順1の呼び出し一覧に pact 未作成の呼び出しが残っていたら作る。逆に消費側が使わない契約は削る。
完了チェック(もれ確認)
- 消費側の全呼び出しが pact 化されたか:手順1のエンドポイント一覧と、pact の interaction 数を突き合わせる。未カバーの呼び出しが残っていないか。
- provider 検証が実際に回っているか:提供側 CI に pact 検証ジョブがあり、緑になっているか。回っていなければ契約は守られていない。
- 必須フィールドが具体例で固定されているか:
like 等の型緩和だけで済ませた必須フィールドが無いか(欠落を見逃す穴)。
- 互換マトリクスが埋まっているか:現行の consumer/provider バージョンの組合せが検証済みか。未検証の組合せをデプロイしようとしていないか。
- 死んだ契約が無いか:消費側がもう呼ばないのに残っている interaction が無いか(提供側を不要に縛る)。
API スキーマ検証
概要
API のリクエスト/レスポンスが、宣言済みスキーマ(OpenAPI、JSON Schema)に構造として準拠しているかを検証する。
コントラクトテスト(Pact)が「消費側が実際に依存する値」を固定するのに対し、こちらは「宣言された型・必須フィールド・制約」への準拠を機械的に検証する点が違う軸(コンシューマ駆動 vs スキーマ駆動)。
目的/いつ使う
OpenAPI/JSON Schema 定義を持つ API で、実装がスキーマからドリフトしていないかを検証したいとき。
スキーマがそもそも無い、または安定しない初期段階の API には過剰。
コントラクトテストと排他ではなく併用できる(スキーマ準拠 + 消費側の実利用値固定の二段構え)。
TypeScript example
import { describe, it, expect } from "vitest";
import Ajv from "ajv";
import openapiSchema from "./openapi.json";
import { handleGetUser } from "./user-handler";
const ajv = new Ajv();
const responseSchema = openapiSchema.paths["/users/{id}"].get.responses["200"].content["application/json"].schema;
const validate = ajv.compile(responseSchema);
describe("GET /users/:id: スキーマ準拠", () => {
it("レスポンスが OpenAPI スキーマに準拠する", async () => {
const res = await handleGetUser({ id: "1" });
const valid = validate(res.body);
expect(valid, JSON.stringify(validate.errors)).toBe(true);
});
});
落とし穴
- スキーマ自体が古いまま放置され、実装との乖離に誰も気づかない。スキーマの更新をレビュー必須にする。
- 型・必須フィールドの構造だけを見て、値の意味的正しさ(在庫が負にならない等)を見た気になる。意味的正しさは同値分割・境界値・PBT の領分。
遂行手順(着手→完了)
- 検証対象のエンドポイントを列挙する:スキーマ定義があるエンドポイント全てを対象にする。
- スキーマバリデータを用意する:OpenAPI なら該当パスのレスポンス/リクエストスキーマを抽出し、Ajv 等の JSON Schema バリデータでコンパイルする。
- 実際のリクエスト/レスポンスをスキーマで検証する:各エンドポイントの代表ケース(正常系・エラー系)を実行し、実際の入出力をバリデータに通す。
- スキーマ変更をレビューゲートに載せる:スキーマファイルの変更時に、対応する実装・テストも変わっているかをレビューで確認する運用にする。
完了チェック(もれ確認)
- 全エンドポイントが検証対象か:手順1の列挙数とスキーマ検証テストの数が一致する。
- エラーレスポンスも検証しているか:正常系のスキーマだけでなく、エラー時のレスポンス形もスキーマ定義があれば検証しているか。
- スキーマの陳腐化が無いか:スキーマファイルの最終更新日と実装の変更履歴が大きく乖離していないか。
「網羅」の扱い
- 網羅基準:スキーマ定義を持つ全エンドポイントの代表的な入出力(正常系・エラー系)がスキーマ準拠を検証されている。
- 達成チェック:バリデーションエラーの詳細(
validate.errors)がテスト失敗時に読めるか。意味的正しさとの境界を混同していないか。
サービス内部の粒度(単体、結合)は levels.md、配分・階層化は strategy-allocation.md を参照。
非機能の観点としてのコントラクト(互換性維持の運用)は nonfunctional-attributes.md にもある。