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の「いい感じ」が怖いという話から始めたのだった。
仕方がない。そこはやる。