logoChibiham
cover
🛡️

型によるガードレールとテストの役割:AI時代のコード品質戦略

導入:AI開発における「テストをガードレールにする」という潮流

コーディングエージェントの普及に伴い、「テストをガードレールにする」という考え方が広まっている。AIが生成するコードの品質を担保するために、テストを先に書き、AIの出力がテストを通過することをもって正しさの基準とする——いわゆるテスト駆動的なアプローチだ。

しかし、この考え方には暗黙の前提がある。テストがプロダクトの構造的な正しさを保証できる、という前提だ。

本稿では、コンセプトモデルとプロダクションモデルの枠組みを土台に、コードベースにおける「構造」と「振る舞い」の区分を明確化し、型とテストそれぞれが担うべき役割を整理する。そして、AI開発において真に有効なガードレール戦略とは何かを考察する。

前提:プロダクションモデルの具体的反映としてのコードベース

開発者の認知は「あるべき姿」としてのコンセプトモデルと「現状の理解」としてのプロダクションモデルの二層構造を持つ。コードベースは、このプロダクションモデルの具体的な外化である。

ここで「コードベース」を構成する要素を分解してみる。

  • 実装コード — モジュール構造、型定義、関数の依存関係を通じて、プロダクションモデルの構造を直接的に表現する
  • テストコード — 特定の入力に対する期待される出力を記述し、プロダクションモデルの振る舞いを検証する

実装コードは構造を表現し、テストコードは振る舞いを検証する。この区分は当たり前に聞こえるかもしれないが、「テストをガードレールにする」という議論において、この区分が曖昧なまま語られていることが多いように思う。

構造とは何か:概念間の結合から生まれる制約

本稿で「構造」と呼ぶものを明確にしておきたい。ここでの構造とは、概念間の結合・依存関係・境界から生まれる制約の総体を指す。

  • モジュール間の依存関係と境界
  • 概念間の結合によって生まれる不変条件(例:「出荷済み注文には支払い日時が必ず存在する」)
  • 技術的制約(プロトコル、データフォーマット等)

重要なのは、ここでは概念単体の内部的な制約(「金額は0以上」のような単純なバリデーション)ではなく、概念間の関係性から生まれる制約に焦点を当てているということだ。

継続的に改善されるプロダクトにおいて、モジュールの結合のさせ方、モジュールの捉え方——すなわちモデル——は移りゆくものであり、意図的に移していくものだ。リファクタリングとは本質的に、この構造の再編成に他ならない。

型は構造を表現し、テストは振る舞いを検証する

型による構造の表現

型システムは、概念間の関係性と制約をコードレベルで表現する手段だ。Make Illegal States Unrepresentable(不正な状態を表現不可能にする)の原則に従えば、型定義そのものがプロダクションモデルの構造を反映する。

typescript
// 構造が弱い — 不正な状態が表現可能
type Order = {
  status: string;
  paidAt: Date | null;
  shippedAt: Date | null;
};
// status が "paid" なのに paidAt が null という不正状態を許してしまう

// 構造が強い — 不正な状態がコンパイル時に排除される
type Order =
  | { status: "draft" }
  | { status: "paid"; paidAt: Date }
  | { status: "shipped"; paidAt: Date; shippedAt: Date };
// 各状態で存在すべきフィールドが型レベルで強制される

後者の設計では、「支払い済みなのに支払い日時がない」という状態をテストで検証する必要がない。コンパイラが排除してくれる。これは代数的データ型型駆動開発の考え方に基づくものだ。

テストによる振る舞いの検証

一方、テストが担うべきはインターフェースに現れる振る舞いの検証だ。ここで重要な区分がある。

実装詳細に結合したテストは、リファクタリングで構造が変わると壊れる。しかしこれはテストという手法の限界ではなく、テストの書き方の問題だ。モジュールの内部実装ではなく、モジュールが外部に公開するインターフェースに対してテストを書いていれば、内部構造のリファクタリングでは壊れない。

つまり、テストの対象はモジュールのインターフェースに現れる振る舞いであるべきで、内部の個々の実装関数に対するテストは構造変化に対して脆い。

フラクタル的なインターフェーステストの構造

この「インターフェースに対してテストする」という原則は、フラクタル的にすべての粒度で適用できる。

フラクタル的なインターフェーステストの構造

各層のインターフェースに対して振る舞いを検証し、内部構造は型で保証する。この構造が入れ子状にすべてのスケールで繰り返される。

サービス境界で行われるE2Eテストも、この視点から見れば特別なものではない。HTTPなどの通信プロトコルという技術的制約との結合結果として現れるインターフェースに対する振る舞いテストであり、ドメインモジュールのインターフェーステストと本質的に同じ構造を持つ。

型で表現しきれない制約の扱い

ここで一つの反論を検討しておく。すべての構造的制約が型で表現できるわけではないという点だ。

例えば「注文は在庫がある商品に対してのみ作成できる」という制約。これは概念間の結合から生まれる不変条件だが、実行時の状態に依存するため、型だけでは表現しきれない。Phantom TypeやBranded Typeである程度は可能だが、実行時検証が不可避な制約は存在する。

この種の制約に対しては、その制約が現れるcontext boundaryのインターフェースで統合テストとして検証するのが妥当だ。「注文は在庫がある商品に対してのみ作成できる」のレベル感であれば、注文と在庫の関係にはcontex boundaryがあり、サービスの境界になるべき箇所だろう。そのインターフェースに対して振る舞いテストを書く。

整理すると:

制約の種類保証手段
概念間の構造的制約(静的)型システム出荷済み注文には支払い日時が必須
実行時状態に依存する制約インターフェーステスト在庫がある商品のみ注文可能

両者は二者択一ではなく、補完的な関係にある。

構造が良ければテストは肥大化しない

ここで一つの直感について検討したい。構造が良ければ、表面的な振る舞いのパターンも限定的で、テストも肥大化しないのではないか

この直感は、二つのメカニズムを通じて成立すると考えている。

メカニズム1:偶有的複雑性の排除

Fred Brooksは「銀の弾丸症候群」で、ソフトウェアの複雑性を本質的複雑性(ドメインに内在する複雑性)と偶有的複雑性(技術的選択や設計の不備から生じる複雑性)に分けた。

型によって不正な状態を表現不可能にすれば、テストで検証すべき状態空間が縮小する。先のOrder型の例で見たように、型が状態空間を制約するほど、テストのケース数は減る。これは偶有的複雑性から生まれるテストケースの削減だ。

メカニズム2:本質的複雑性の局所化

一方、ドメインに内在する本質的複雑性は、構造の良し悪しに関わらず残る。例えば税率計算——商品カテゴリ、地域、顧客種別、キャンペーン適用の組み合わせで税率が変わるような場合、型で各概念を明確に分離しても、組み合わせのパターン自体は減らない。

しかし、構造が良ければ、この本質的複雑性は局所化される。税率計算モジュールの中に閉じ込められ、その境界の外には漏れ出さない。モジュール内ではテストが多くなるかもしれないが、プロダクト全体から見れば、テストは肥大化していない。

「構造が良い」ということは、概念間の責務分離が明確で、各インターフェースが限定された関心事のみを扱っているということだ。限定された関心事は、限定された入出力パターンを生む。

逆に言えば、テストが肥大化するのは、一つのインターフェースが多すぎる関心事を引き受けている兆候だ。テストの肥大化は構造の問題のシグナルであって、テストの問題ではない。

AI開発への含意:真のガードレール戦略

以上の整理を踏まえて、AI開発における「ガードレール」の議論を再検討する。

現在の「テストをガードレールにする」論は、暗黙に単体テストを大量に書いてAIの出力を縛ることを想定している場合が多い。しかし本稿の議論を踏まえると、いくつかの問題が浮かび上がる。

問題1:実装詳細に結合したUTはAIのリファクタリングを阻害する

AIがコードを生成・修正する際、内部構造のリファクタリングは頻繁に起こる。実装詳細に結合したUT(ユニットテスト)は、このたびに壊れてノイズになる。AIは壊れたテストを直すことに労力を費やし、本来の構造改善に集中できない。

問題2:コンテキストウィンドウの浪費

AIのコンテキストウィンドウは人間のワーキングメモリに相当する制約を持つ。大量のUTをコンテキストに入れるより、型定義とインターフェーステストを入れた方が、プロダクションモデルの構造をより効率的に伝達できる。

型定義は構造の宣言的な表現であり、少ないトークンで多くの制約を伝える。一方、UTは個別の入出力ケースの羅列であり、同じ情報量を伝えるのに多くのトークンを消費する。

問題3:構造の保証がテストに依存すること自体のリスク

テストは「通過するか否か」の二値判定であり、なぜその構造であるべきかの意図は伝わらない。型は構造そのものを表現するため、AIが新しいコードを生成する際にも、構造的な制約が自動的に適用される。

提案:型とインターフェーステストの二層ガードレール

本稿の議論から導かれるガードレール戦略は以下のようになる。

第一層:型による構造の保証

  • Make Illegal States Unrepresentableの原則に従い、不正な状態を型レベルで排除する
  • モジュール間の依存関係と境界を型で表現する
  • コンパイラがガードレールとして機能する

第二層:インターフェーステストによる振る舞いの検証

  • 各モジュールの公開インターフェースに対して振る舞いを検証する
  • 実行時状態に依存する制約は、context boundaryのインターフェースで統合テストとして検証する
  • E2Eテストも含め、フラクタル的に各層のインターフェースで行う

この二層構造は、コンセプトモデルとプロダクションモデルの枠組みで言い直すと:

  • 型 = プロダクションモデルの構造そのものの外化
  • インターフェーステスト = その構造が生む振る舞いの検証

コードベースにプロダクションモデルを反映する際、構造の正しさはコンパイラが保証し、振る舞いの正しさはインターフェーステストが保証する。

結論:型を第一のガードレールに

AI時代のコード品質戦略として、「テストをガードレールにする」という方針は半分正しく、半分間違っている。

正しい部分は、コードの正しさを機械的に検証可能な手段で担保するという方向性そのものだ。AIが生成するコードを人間がすべてレビューするのは現実的ではなく、自動化された検証手段が不可欠である。

間違っている部分は、その手段として単体テストを第一に置くことだ。構造の保証には型の方が適しており、テストは振る舞いの検証としてインターフェースレベルで行うべきだ。

型による構造の保証とインターフェーステストによる振る舞いの検証——この補完的な二層構造が、AI開発における真のガードレール戦略ではないだろうか。そしてこの戦略は、偶有的複雑性を型で排除し、本質的複雑性をモジュール内に局所化することで、テストの肥大化を防ぎ、コードベースの進化可能性を維持する。

仕様・実装共進化のアプローチにおいても、型は構造の進化を安全に支えるレールとなり、インターフェーステストは進化の結果を検証するチェックポイントとなる。両者があることで、コンセプトモデルとプロダクションモデルの高速な往復が可能になるのだと考えている。