カバレッジ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はその橋渡しになる現実解だと思う。