Claude Code や Copilot でコードを生成するようになって、テストをどうするか、が前より気になるようになりました。

「カバレッジ100%を目指す時代は終わったのかもしれない」なんて言うと大げさですが、普通のユニットテストを書いていて、なんか物足りない感じがありました。思いついた入力パターンしかテストしていない、というか。

そこで、Property Based Testing(PBT)というのを試してみることにしました。

なぜ従来のユニットテストでは足りないのか

AI生成コードの特徴として、エッジケースの網羅が甘いことが多い。人間が書くテストも同様で、「思いついた入力パターン」しかテストしない。

// よくあるテスト
test('add positive numbers', () => {
  expect(add(1, 2)).toBe(3);
  expect(add(0, 0)).toBe(0);
});
// これで本当に十分?

この「思いついた入力パターンしかテストしない」のが問題だな、と。add(1, 2) が通っても、負数や小数や大きな数でどうなるかは見えていません。

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] ← 最小化された入力

これがデバッグ効率を劇的に上げる、というのは実感できました。失敗が「長さ100の配列で起きた」ではなく「[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

実際、AIが生成したソートや計算ロジックにPBTを当てたら、境界条件で失敗するケースが何度かありました。生成コードほど、性質ベースのテストが効く気がします。

PBTの限界

万能ではないです。以下のケースは従来のテストの方が適している、というのが今の感触です。

状態を持つシステム

DBやファイルシステムを絡めたテストは、PBTだとセットアップが複雑になりすぎる。stateful testingという手法もあるが、学習コストが高い。

外部APIとの連携

モックの組み合わせ爆発が起きやすい。契約テスト(Pact等)の方が現実的。

UIの振る舞い

「ボタンを押したら画面遷移する」のような振る舞いは、性質として定義しにくい。

実務での使い分け

今のところ、こんな使い分けで回しています。

  • 純粋関数・計算ロジック: PBTで性質をテスト
  • 状態管理・副作用: 従来のユニットテスト
  • UI/表示系: スナップショットテスト
  • 統合テスト: 最小限のE2E

カバレッジは指標として見るが、100%を目指す工数は他に回す。

おわり

PBTが全部の答えではないです。ただ、AIが生成するコードが増えると、「思いついた入力パターン」のテストだけでは不安で、性質を書く方がしっくりきました。

今なら、AIが生成した関数の検証は、デフォルトでPBTを挟むのが良さそうです。言語ごとのツールは揃っているので、試すハードルも低めです。もっと慣れてきたら、stateful testingにも手を出してみたい。