TDDにおける仕様の不在の問題
テスト駆動開発はなぜ普及し、なぜAI時代に限界を迎えるのか
TDDはなぜ普及したか — 人間の認知との相性
TDDが広く普及した背景には、人間の認知特性との優れた相性があります。「この入力に対して、この出力が返るべきだ」という具体的な例示は、抽象的な仕様を書くよりもはるかに直感的です。数学的な不変条件や事前条件を形式的に記述するよりも、具体的なinput/outputの組み合わせを列挙する方が、多くの開発者にとって自然な思考プロセスに沿っています。
また、TDDが主に普及したWeb開発という領域の特性も無視できません。Webアプリケーションは、ミッションクリティカルなシステムと比較して、バグに対する許容度が相対的に高い分野です。致命的なバグがあっても即座にデプロイし直せる。ユーザーがリロードすれば解決する。決済処理のような一部を除けば、厳密な正しさよりも素早いイテレーションが優先される世界です。
この環境では、「完璧な仕様を事前に定義する」コストよりも「素早くテストを書いて動かしながら修正する」アプローチの方が経済合理性がありました。TDDの普及は、手法としての本質的な優位性というよりも、Web開発という特定の文脈における実用性の高さに起因する部分が大きいのです。
仕様はどこにあるのか — TDDの構造的欠陥
TDDの「テストを先に書く」という原則は、一つの根本的な問いを回避しています。そのテストの正しさは、何によって保証されるのでしょうか。
テストケースは仕様の断片です。しかしTDDでは、その仕様は具現化されていません。テストを書く開発者の頭の中に、暗黙的に、断片的に、おそらく不完全な形で存在しているだけです。開発者が「このAPIはこう動くべきだろう」と考えた振る舞いがテストになりますが、その「べき」の根拠は主観的な理解に過ぎません。
ここで重要なのは、テストそのものが仕様の代替物として機能しているという構造です。TDDの世界では「テストは実行可能な仕様である」とよく言われますが、これは論点のすり替えです。テストは仕様の一部を反映した検証手段であり、仕様そのものではありません。有限個のinput/outputの組み合わせは、仕様が定義する無限の入力空間のごく一部をサンプリングしたものに過ぎません。
テストの品質はテスト作成者の知識と経験に完全に依存します。経験豊富な開発者はエッジケースを思いつきますが、それでも「思いつかなかったケース」はテストされません。そして、テスト作成者が複数人いる場合、各人の暗黙的な仕様理解が異なる可能性があります。Aさんは「空文字列はエラー」と理解し、Bさんは「空文字列は許容」と理解している — このような不整合がテストスイート内に静かに共存し、発見されないことがあります。
形式仕様が存在すれば、この矛盾は仕様レベルで検出可能です。VDM-SLで「名前は空文字列不可」と書かれていれば、それに反するテストは仕様違反として明確に識別できます。しかしTDDでは、テスト同士の整合性を保証するメカニズムが存在しません。
既存コードからのテスト生成 — バグの制度化
TDDの理想は「テストを先に書く」ですが、現実のプロジェクトでは、既存コードに対して後からテストを追加するケースが非常に多いのが実情です。レガシーコードのリファクタリング、引き継ぎプロジェクト、あるいは単にテストなしで書き始めてしまったコード — こうした場面でテストを書く際、開発者は何を参照するでしょうか。
答えは「現在の実装」です。コードを読み、振る舞いを理解し、その振る舞いを再現するテストを書きます。このプロセスには構造的な欠陥があります。
コードにバグがある場合、そのバグを含んだ振る舞いが「正しい仕様」としてテストに組み込まれます。例えば、消費税計算で切り捨てるべきところを切り上げているコードがあった場合、テスト作成者はその切り上げの結果を「期待値」として記述します。テストは通りますが、ビジネスルール上は間違っています。
# 既存コード(バグ:切り捨てるべきところを切り上げ)
def calc_tax(price):
return math.ceil(price * 0.1) # 本来は floor
# テスト作成者がコードから仕様を推測
def test_calc_tax():
assert calc_tax(105) == 11 # テストは通る。しかし正しくは10このテストは「コードが現在の振る舞い通りに動くこと」を保証しているだけで、「コードが正しく動くこと」は保証していません。テストがグリーンであることは、正しさの証拠ではなく、現状維持の証拠に過ぎません。
「テストがあるから安心してリファクタリングできる」というのはTDDの大きなメリットとされていますが、テスト自体がバグを含む仕様に基づいている場合、リファクタリングで「テストが通る」ことは「バグが保存されている」ことと同義です。テストが「安全網」として機能するのは、テストの根拠となる仕様が正しい場合に限ります。
AI時代にTDDが適合しない理由
上述したTDDの構造的問題は、人間中心の開発では「許容可能な限界」として受け入れられてきました。テストを書く人間がドメイン知識を持ち、暗黙的な仕様を頭の中に保持し、テストレビューで不整合を指摘し合う — こうした人間の判断力で問題を軽減できていたからです。
しかし、AIが開発の中心になる時代には、この前提が根本から崩れます。
人間のチームでは、ドメイン知識が暗黙的にチーム内で共有されます。「この業界ではこういう計算が慣例」「このクライアントはこの仕様にこだわる」といった知識は、明文化されていなくても会話やレビューを通じて伝達されます。異なるモデル・事業者・バージョンのエージェント間では、この共有が成立している保証がありません。会話やレビューを通じた暗黙の擦り合わせという、人間チームの補正メカニズムも働きません。
マルチエージェント開発では、複数のAIエージェントがそれぞれのモジュールを担当します。Agent Aが認証を、Agent Bが決済を、Agent Cがフロントエンドを担当する場合、各エージェント間のインターフェースの仕様は厳密に合意されている必要があります。TDDではこの合意を実現できません。各エージェントが「自分の理解」に基づいてテストを書き、それぞれのテストが通る — しかし結合すると動かない。仕様の具現化がないことはAI同士の連携において致命的です。
テストはあくまでも個々のモジュールの振る舞いを検証するものであり、モジュール間の契約を定義するものではありません。「Agent Aの出力はこの型で、この事前条件を満たす」「Agent Bはこの入力を受け取り、この事後条件を保証する」— このような契約は、テストケースの集合ではなく、形式仕様として明示的に定義されるべきです。
形式手法による構造的解決
形式手法は、TDDの問題を根本から解決します。仕様をVDM-SLなどの形式言語で記述することで、仕様が具体的な成果物として存在するようになります。テストケースは仕様から導出されるものであり、開発者の暗黙知に依存しません。
仕様の具現化
暗黙知ではなく、数学的に厳密な記法で仕様を明文化する。仕様自体が検証可能な成果物になる。
テストの導出
テストケースは仕様から体系的に導出される。仕様が定義する全空間をカバーする戦略が立てられる。
エージェント間の契約
各エージェントのインターフェースが事前条件・事後条件・不変条件として形式的に定義される。結合前に契約の整合性が検証可能。
バグの固定化防止
テストの根拠が形式仕様であるため、コードのバグがテストに伝播しない。仕様とコードの乖離は検出可能。
TDDは人間がWeb開発を行うという特定の文脈において優れた実用性を持つ手法でした。しかし、AIが開発の中心になり、システムの複雑性が増し、エージェント間の正確な仕様合意が求められる時代には、仕様の具現化を前提とするアプローチが不可欠です。形式手法はTDDを否定するものではなく、TDDが暗黙的に前提としていた「正しい仕様の存在」を明示的に提供するものです。