GraphQL コントラクトテスト
概要
レイヤー間の継ぎ目に位置し、一方が他方の同意なしに動いたときに壊れるテスト — 境界に適用された「テスト・アズ・ア・コントロール」。基本ルール:契約(コンシューマーが依存するデータ形状とアクセスセマンティクス)をアサートし、クエリ文字列自体は決してアサートしない。 見た目のクエリ編集で壊れるテストは変更検出アンチパターンであり、保証された形状や権限が動いたときに壊れるテストがコントロールである。
§1 — GraphQL クエリ契約
- すべての操作は、既知の場所に型付きドキュメントとして一度だけ作成され、エクスポートされてインポートされる。 UIはそれをインポートし、テストはまったく同じアーティファクトをインポートする — 決して再入力したコピーではない。テストにクエリをコピペすると契約が静かに破壊される(UIクエリを編集してもテストが壊れなくなる)。DRYが耐荷重になる。
- 構造上、双方向に壊れる:
- DB → UI のズレ:コンパイル時に。
graphql-codegenがライブスキーマからTypeScript型を生成する。使用中のフィールドをリネームまたは削除すると、tscがすべての使用箇所 および 共有ドキュメントで失敗する — ユーザーが空白のパネルを見る前にビルドが壊れる。これがライブスキーマに対するコード生成が必須である理由である。 - UI → DB / パーミッションのズレ:実行時に。 共有ドキュメントが、実際の呼び出しパスを通じてシード済みテストDBに対して、ロールコンテキスト下で実行され、返された形状とアクセス結果(クエリテキストではない)をアサートする。
- DB → UI のズレ:コンパイル時に。
- コントラクトテストとRLSロールマトリックスは同じハーネスである。 一つのドキュメントをPostGraphileを通じてロールA(データを期待)およびロールB(拒否/空を期待)として実行する。一つのメカニズムが3つのことを証明する:クエリがまだスキーマに一致する、UIの前提がまだ有効である、RLSがまだ境界を強制している。
実例 — 実際のバグがどのように捕捉されるか
- DB→UI: マイグレーションで
equipment.is_activeが削除される。Codegenが新しいスキーマから再生成し、生成された型からフィールドが失われる。tscが共有ドキュメントとそれを使用するすべてのコンポーネントで失敗する — マージ前にビルドで捕捉される。 テストは「クエリテキスト == X」をアサートしない。 - UI→DB: 開発者がUIクエリを拡大して
cancellation_reasonを取得しようとする(RLSは昇格ロールにのみ公開)。一般メンバーとして実行されるランタイムテストがメンバー可視形状をアサートし、null/拒否を得て失敗する — 「これを公開する意図があったのか?」を強制する。
§2 — ルートコントラクトとスモークテスト
- ルートは単一ソースから列挙される。 各ルートにはスモークテストがある:エラーなくレンダリングされる + 最小限の必須要素セット(見出し、ランドマーク、主要なコントロール — 詳細なインタラクションは不要)を表示する。
- ルートの追加/削除は対応するテスト変更を強制する — 新しいURLはスモークテストなしでは不完全であり、削除されたURLはそのテストを削除する。URLが黙って出現したり消失したりすることは決してない。
- 明示的に詳細なUI/インタラクションテストではない — 意図的で範囲を限定。問題は「このURLは機能し、エッセンシャルをレンダリングするか」であり、それ以上ではない。
強制手段: コンパイル時には graphql-codegen + tsc をCIで(ブロッキング)実行。ランタイムのコントラクトテスト + ルートスモークテストはシード済みDBに対してCIで実行。
完全な根拠:憲法第15条、engineering-constitution/references/engineering-constitution.md にバンドル。