tech
tech 2026.07.19 7 min read

AIとテスト駆動

AIと一緒にコードを書くとき、テスト駆動はどう効くのかを考える。

AIにコードを書いてもらうと、テストが急に頼もしく見える。

自分で実装していたときも、もちろんテストは大事だった。でも正直なところ、あとで書こうと思っていた。あとで、とは、機能が動いて、レビューが終わって、障害が起きる少し前くらいのことである。だいたい来ない。

AIは速い。

こちらが要件を一段落書くと、実装が出る。修正を頼むと、別の実装が出る。さらに頼むと、最初からそうするつもりだったような顔で三つ目が出る。

この速さを前にすると、テストは足枷に見えるかもしれない。

先にテストを書く時間があるなら、そのままAIに実装させたほうが速いのではないか。動かして、ダメなら直せばいい。AIは疲れないし、何回でもやり直せる。

でも最近は、AIが速いからこそ、先にテストがあったほうが速いと思うようになった。

速いものには、走る方向がいる

AIに「いい感じに実装して」と頼むと、かなりの確率でいい感じの何かが出てくる。

問題は、こちらのいい感じと、AIのいい感じが同じとは限らないことだ。

たとえば、クーポンの有効期限を判定する処理を頼む。

期限が7月31日なら、31日の23時59分まで使えるのか。31日になった瞬間に切れるのか。タイムゾーンはどこか。期限が未設定なら無期限なのか、無効なのか。

人間同士なら会話の途中で「そういえば」と出てくる話も、AIは出てきた情報だけで、ひとまず筋の通った実装にする。筋が通っているので余計に困る。違う道を、いい姿勢で走っていく。

そこで先に振る舞いを書く。

it('期限日の23時59分まではクーポンを使える', () => {
  const coupon = { expiresOn: '2026-07-31' }
  const now = new Date('2026-07-31T14:59:59Z')

  expect(canUseCoupon(coupon, now, 'Asia/Tokyo')).toBe(true)
})

大事なのは、AIが走る方向が一つ決まったことだ。「期限日を含む」という日本語だけでは消えていた時刻とタイムゾーンが、具体的な例になっている。

AIはこの境界の中でなら、かなり自由に走れる。

日付ライブラリを使う案でもいい。単純な比較にする案でもいい。既存の時刻ユーティリティを見つけて使ってもいい。こちらが毎回ハンドルを握らなくても、境界を越えたらテストが赤くなる。

テストはブレーキではない。

コースの外側にある壁に近い。壁があるから、アクセルを踏める。

赤は、AIへの説明になる

この赤が、AIと作業するときはかなり便利だ。

「クーポン期限の扱いを直してください」だけだと、AIはコードを読み、意図を推測し、変更箇所を選ぶ。推測が何段も入る。

一方で、失敗するテストがあると話が早い。

この入力で、この結果にしたい。今はこう失敗している。既存のテストは壊さない。

説明がコードとして置かれていて、実行もできる。AIが「対応しました」と言ったあと、本当に対応したかをこちらの機嫌に関係なく確認できる。

人間は夕方になると判定が甘くなる。「まあ動いてそう」が増える。AIはいつでも自信がある。夕方の人間と常時自信満々のAIを会議室に入れると、だいたい勢いでマージされる。

しかも、失敗が具体的ならAIは直すのも速い。実装して、テストを走らせ、エラーを読み、修正する。このループを細かく回す仕事はAIと相性がいい。

人間が一行ずつ指示するより、期待する振る舞いを置いて、短い距離を何度も走ってもらうほうがいい。

手綱というより、フィードバックが返る周回コースである。

テストもAIが書けばいい、の罠

実際、任せる。自分もかなり任せている。

境界値を挙げてもらう。既存のテスト形式に合わせてもらう。正常系を書いたあと、「抜けている異常系はある?」と聞く。退屈なセットアップを作ってもらう。助かる。テストの雛形を手で並べる人生には、できれば戻りたくない。

ただ、実装したAIに、その実装が正しいことを示すテストまで丸ごと頼むと、妙にきれいな世界ができることがある。

AIが「期限日は使えない」と解釈して実装する。

同じAIが「期限日には使えない」というテストを書く。

全部緑になる。

よかったですね、ではない。

実装とテストが同じ勘違いを共有しているだけだ。二人組に見えるが、二つの証言の口裏を合わせた一人である。

もっと露骨なこともある。落ちているテストを通して、と頼んだら、実装ではなくテストの期待値を変える。難しい分岐を通らないモックにする。関数が呼ばれたことだけ確認して、結果は見ない。

AIに悪意があるわけではない。与えられた仕事を「テストを緑にすること」と読めば、最短距離を見つけただけだ。ゴールテープを手前に持ってきてから走り抜ける。賢い。そうじゃない。

だから、自分が大事だと思う振る舞いのテストは、実装より先に置くか、少なくとも実装とは分けて確認したい。

AIにテスト案を出してもらってもいい。でも、どの期待値を採用するかは人間が見る。テストが赤い状態を一度確認する。実装を変えずにテストだけ緑になっていないか、差分を見る。

このひと手間で、テストはAIの自己採点から、こちらとの契約に少し近づく。

実装を縛るテストは、手綱が短すぎる

AIにテストを書かせると、実装の形をそのまま固定するテストが出てくることがある。

内部のhelperが三回呼ばれること。特定の順番でメソッドを呼ぶこと。途中のオブジェクトがこの形になること。privateな処理を無理やり外から触ること。

そういうテストは、いまの実装を詳しく説明している。でも、利用者にとって何が正しいかはあまり説明していない。

そして次にAIへリファクタを頼むと、きれいに壊れる。

振る舞いは同じなのに、helperをまとめただけで赤。順番を変えただけで赤。AIは大量の失敗を見て、今度はテストに合わせて古い構造を復元し始める。

自由に走らせるための境界だったはずが、足首に巻きついている。

クーポンの例なら、確認したいのは「この日時、この条件なら使える」ことだ。内部で日付を何回変換したかではない。

もちろん呼び出しそのものが仕様になる場所もある。課金APIを二回叩かない、監査ログを必ず残す、メール送信を再試行しても重複させない。そこは観測する意味がある。

でも、内部の動きを全部固定すると、AIの得意な別案の探索まで殺してしまう。

手綱は、進む方向を伝えるためにある。歩幅まで全部決めるものではない。

何をテストするかは、仕様を決めること

何をテストするか決めることは、何を正しいとするか決めることでもある。

期限日にクーポンを使えるのか。返金済みの注文にポイントを戻すのか。権限がない人には404を返すのか、403を返すのか。通信が切れたとき、再試行するのか、利用者に選ばせるのか。

AIは候補を出せる。一般的な慣習も教えてくれる。抜けていそうな境界を並べるのも得意だ。

でも、顧客との約束や、運用の都合や、失敗したときに誰が困るかまでは、プロンプトに入っていなければ知らない。入っていても、最後に選ぶのはこっちだ。

AIとやると、むしろ「先に決める」の重さがよく見える。

実装は一瞬で出る。だから、その前に置く小さな期待値が、以前よりずっと強くコードを引っ張る。雑な仕様を渡せば、雑さまで高速に実装される。

速いのは、正しい方向に進むときだけ嬉しい。

AIを自由に走らせるために

毎回、厳密なTDDをやれているわけではない。

先に実装してしまう日もある。動いたあとでテストを書くこともある。小さな文言変更に赤、緑、リファクタと唱え始めたら、たぶん少し休んだほうがいい。

それでも、判断が分かれそうな境界、壊れたら困る振る舞い、過去に一度やらかした場所には、先に具体例を置くようにしている。

テストを人間だけで書く必要はない。AIに案を増やしてもらえばいい。実装も、修正も、リファクタも、かなり任せていい。

ただし、テストと実装が同じ思い込みで緑になっていないかを見る。内部構造ではなく、守りたい振る舞いを囲う。何を正しいとするかは、自分たちで決める。

そこまでできると、テストはAIの足枷ではなくなる。

どこまで走っていいかを示す境界になり、間違った方向へ進んだことをすぐ返すフィードバックになり、こちらが細かく操縦しなくても済む手綱になる。

なので、ゆっくり走らせるのではなく、速く走っても大丈夫なコースを作りたい。

壁の外に何があるかを決めるのは、まだ人間の仕事だ。

できればそこもいい感じに決めてほしい気持ちはある。

でも、AIの「いい感じ」が怖いという話から始めたのだった。

仕方がない。そこはやる。