ベンチャー開発のような、テストなんてろくに書いてこなかった。バグが出たら直す、それで回してきた方へ。。。
このやり方、運用チームの負担が尋常じゃない。バグ対応に追われて、適当にマージして「結果動かないじゃないか!」となった案件は多いのではなかろうか?
ましてやAIコーディングの時代なのでますます運用が難しくなっているだろう。
で、思ったわけだ。コーディングAgentがいる時代に、人間がテストを書く意味ある?
テストは全部Agentに書かせて、人間は仕様だけ見る——そういう運用にゴリ押しでもよくないか、と。
この記事は、その発想で「運用者が限界まで手を抜くにはどうするか」を突き詰めた話だ。
鍵は、AIに書かせたテストの “充足”をどう担保するか。結論から言うと、レビューを「実装」から「テスト」へ移し、その十分さは「カバレッジを測る」だけで大きく前進する。
そもそも、テストのカバレッジって「測れる」の知ってた?
「テスト書いてる」けど「どれだけ網羅できてるかは測ったことない」という方は多いのではなかろうか?
vitestなら --coverage を足すだけ。
npx vitest run --coverage
すると、どの行・どの分岐をテストが通っていないかが表でバッと出る。これだけで「自分のテスト、ここ抜けてたんだ」が目で見えるようになる。AIにテストを書かせた時こそ、これが効く。
実際にやってみよう。
題材:ありがちな送料計算
// 仕様: 5000円以上は送料無料。北海道・沖縄は1000円、それ以外は500円。
export function shippingFee(prefecture: string, amount: number): number {
if (amount >= 5000) return 0;
if (prefecture === "北海道" || prefecture === "沖縄") return 1000;
return 500;
}
「ちゃんとテストして」だと人もAIも漏らすので、テストケースは機械的に並べる。要は境界と分岐を全部突くだけ。
| prefecture | amount | 期待 |
|---|---|---|
| 東京 | 5000 | 0 ← 無料の境界ちょうど |
| 東京 | 4999 | 500 ← 境界の外 |
| 北海道 | 3000 | 1000 ← 離島 |
| 沖縄 | 3000 | 1000 ← 離島 |
| 東京 | 3000 | 500 ← 通常 |
この表が、このあと全部の「正」になる。人間がレビューするのはこの表だけでいい、というのがこの記事のオチに繋がる。
AIに「とりあえず動く」テストを書かせると、こうなる
AIに雑に振ると、だいたい“それっぽく緑になる”テストが返ってくる。
import { describe, it, expect } from "vitest";
import { shippingFee } from "./shipping";
describe("shippingFee", () => {
it("5000円以上は送料無料", () => {
expect(shippingFee("東京", 5000)).toBe(0);
});
it("通常は500円", () => {
expect(shippingFee("東京", 3000)).toBe(500);
});
});
実行すると——テストは緑。2件ともパス。 普通ならここで「OK」と言ってマージしたくなる。
ここで --coverage を足す。すると、こうだ(実測)。
Test Files 1 passed (1)
Tests 2 passed (2)
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|------------------
shipping.ts | 80 | 83.33 | 100 | 100 | 4
緑なのに、Branchは83%。Uncovered Line #s に「4」。
4行目は 北海道・沖縄なら1000円 の分岐だ。つまりAIのテストは離島ケースを一度も通っていない。緑なのに、仕様の3分の1が抜けていた。
テストが緑かどうかは「書いたテストが通ったか」しか言わない。何をテストし忘れたかは、カバレッジを測らないと見えない。
抜けを埋める
カバレッジが指した「4行目(離島)」と、ついでに境界(4999)を表から足す。
import { describe, it, expect } from "vitest";
import { shippingFee } from "./shipping";
describe("shippingFee", () => {
it.each([
["東京", 5000, 0],
["東京", 4999, 500],
["北海道", 3000, 1000], // ← 抜けてた離島分岐
["沖縄", 3000, 1000],
["東京", 3000, 500],
])("%s %i円 → %i円", (pref, amount, expected) => {
expect(shippingFee(pref, amount)).toBe(expected);
});
});
再実行(実測)。
Tests 5 passed (5)
Statements : 100% ( 5/5 )
Branches : 100% ( 6/6 )
Branch 100%。 これで「仕様の分岐は全部通した」と数字で言える。
ポイントは、カバレッジ100%を目標にするんじゃなく、「抜けを見つける道具」として使うこと。表(仕様)を正として、抜けを潰すために測る。
で、レビューはテストだけ見ればよくなる——が、罠が1つ
ここまで来ると、人間の仕事は「テスト表(=仕様)のレビュー」と「カバレッジの確認」にほぼ集約される。実装の行を追わなくてよくなる。これがやりたかったこと。
ただしでかい罠がある。AIに実装とテストを“同時に”書かせると、テストが実装に寄った“同義反復”になる。実装のバグごとテストが追認して、緑なのに間違う。
対策はシンプル。
- テストは仕様から書く。実装を見せない。 さっきの表だけが正。
- テストを先に確定させてから実装させる(テストファースト)。
- できれば実装とテストを別セッション/別エージェントで書かせる。互いのバグを共有させない。
個人的には、AIに「まずこの表を満たすテストだけ書いて」と渡し、通ることを確認してから実装させるだけで、だいぶ事故が減った。
まとめ(運用フロー)
1. 仕様 → テストケースを機械的に列挙 ← 人間がレビューするのはここ
2. AIにテストを書かせる(実装は見せない/テストファースト)
3. AIに実装を書かせて緑にする
4. vitest --coverage で「抜け」を確認 ← 足りなければ表に足してループ
5. PRは「テスト+カバレッジ」を見る。実装は緑なら通す
ツール:vitest/--coverage(裏は V8 or istanbul)。まずはこれだけでいい。
限界
- E2E・性能・セキュリティ・探索的テストはこの枠の外。
- そして大事な但し書き:カバレッジ100%=完璧、ではない。「全部の行を通った」だけで、「その行が正しいか」までは保証しない。
>=を>に間違えても100%のまますり抜けることがある。- そこを潰すミューテーションテストという次のレイヤーがあり、そういう部分まで行くとコードレビューの対象が大幅に減り、本質的な仕様に重きを置くことができるようになる。
それでも、「実装を全部読む」から「テスト表とカバレッジを見る」に重心を移すだけで、AIコードのレビューは本当に別物になる。