カバレッジ100%を目指す時代は終わったのかもしれない。
AI生成コードが増える中で、従来のユニットテストの価値を再考する機会が増えた。結論から言うと、Property Based Testing(PBT)が現実的な落とし所になりつつある。
なぜ従来のユニットテストでは足りないのか
AI生成コードの特徴として、エッジケースの網羅が甘いことが多い。人間が書くテストも同様で、「思いついた入力パターン」しかテストしない。
// よくあるテスト
test('add positive numbers', () => {
expect(add(1, 2)).toBe(3);
expect(add(0, 0)).toBe(0);
});
// これで本当に十分?
PBTの基本的な考え方
PBTは「性質」をテストする。具体的な入出力ではなく、「常に成り立つべき法則」を検証する。
// fast-check を使った例
import fc from 'fast-check';
test('add is commutative', () => {
fc.assert(
fc.property(fc.integer(), fc.integer(), (a, b) => {
return add(a, b) === add(b, a);
})
);
});
Shrinkingが効く
PBTの真価は「失敗時」に発揮される。テストが失敗すると、ツールは自動的に「最小の失敗ケース」を探してくれる。
// 例: 配列の長さが100で失敗した場合
// shrinkingにより「長さ1で失敗する最小ケース」まで絞り込まれる
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
return myFunction(arr).length <= arr.length;
})
);
// 失敗時: Counterexample: [42] ← 最小化された入力
これがデバッグ効率を劇的に上げる。
言語別ツール選定
TypeScript
- fast-check: 最も成熟している。shrinkingが優秀で、失敗ケースの最小化が速い
Python
- Hypothesis: デファクトスタンダード。Django統合もある
Rust
- proptest: マクロベースで書きやすい。quickcheckより柔軟
AI生成コードとの相性
Claude CodeやCopilotで生成したコードに対してPBTを書くと、驚くほどバグが見つかる。
理由は単純で、AIは「よくあるパターン」を学習しているが、境界条件や数学的性質までは考慮していないことが多い。
# Hypothesisの例
from hypothesis import given, strategies as st
@given(st.lists(st.integers()))
def test_sort_idempotent(xs):
sorted_once = sorted(xs)
sorted_twice = sorted(sorted_once)
assert sorted_once == sorted_twice
PBTの限界
万能ではない。以下のケースは従来のテストの方が適している。
状態を持つシステム
DBやファイルシステムを絡めたテストは、PBTだとセットアップが複雑になりすぎる。stateful testingという手法もあるが、学習コストが高い。
外部APIとの連携
モックの組み合わせ爆発が起きやすい。契約テスト(Pact等)の方が現実的。
UIの振る舞い
「ボタンを押したら画面遷移する」のような振る舞いは、性質として定義しにくい。
実務での使い分け
- 純粋関数・計算ロジック: PBTで性質をテスト
- 状態管理・副作用: 従来のユニットテスト
- UI/表示系: スナップショットテスト
- 統合テスト: 最小限のE2E
カバレッジは指標として見るが、100%を目指す工数は他に回す。
おわり
AI時代のテスト戦略は「網羅性」から「堅牢性」にシフトしている。PBTはその橋渡しになる現実解だと思う。