「このファイルにテストを書いて」と指示すると、いまはかなりの精度でテストが出てきます。網羅率も上がります。ところが数か月運用すると、リファクタリングのたびに大量に赤くなる、壊れた理由が読み取れない、という状態に陥っていることがあります。原因の多くは粒度、つまり何を一つの単位とみなすかの選択にあります。
この回は の粒度に絞ります。書く手間が下がったからこそ、粒度を人間が決めておく必要が出てきた、という話です。
1. 単位を「関数」に取ると壊れる
最も素直な発想は、関数ごとにテストを書くというものです。実際 AI に丸投げすると、この形になりがちです。関数の一覧が入力として与えられているので当然ではあります。しかしこの粒度には根本的な問題があります。関数の分かれ方は実装の都合で決まるという点です。
3つの関数に分けていた処理を2つに統合したとします。振る舞いは何も変わっていません。それでもテストは壊れます。外から見て何も変わっていないのにテストが赤くなるなら、そのテストは変更を守っているのではなく、変更を邪魔していることになります。
壊してはいけないものを壊した(正しい失敗)と、内部構造を変えただけ(無駄な失敗)。後者の比率が上がると、開発者はテストの赤を「またか」と受け流すようになります。粒度の設計は、この比率を下げる作業です。
2. 単位は「観測できる振る舞い」で取る
代わりに使える基準が、外から観測できる振る舞いです。呼び出し側にとって意味のある入出力の組を一つの単位とみなし、その内側がどう実装されていようとテストは関知しない。こうすると、実装の整理をしてもテストは緑のままです。
| 粒度の取り方 | テストの安定性 | 失敗時の分かりやすさ | 向く場面 |
|---|---|---|---|
| 関数ごと | 低い(実装に追随) | 高い | 純粋関数、アルゴリズム |
| 振る舞いごと | 高い | 中 | 大半の業務ロジック |
| モジュールごと | 高い | 低い(範囲が広い) | 外部に公開する境界 |
「振る舞いごと」が万能というわけではありません。範囲が広がるぶん、失敗したときにどこが悪いのかは分かりにくくなります。純粋な計算処理やアルゴリズムのように、関数そのものが振る舞いと一致している場合は、関数ごとで何の問題もありません。
3. モックの使いすぎが粒度を壊す
粒度の問題は、 の使い方と直結しています。特に は「呼ばれ方」を検証する道具なので、使った瞬間にテストが内部構造を知っていることになります。
def test_apply_discount_calls_repository_twice(mocker): repo = mocker.Mock() repo.find.return_value = {"price": 1000} service = PriceService(repo)
service.apply_discount(item_id=1, rate=0.2)
# 内部で repo.find を何回呼ぶかは実装の都合でしかない assert repo.find.call_count == 2 repo.find.assert_called_with(1)このテストは、キャッシュを入れて呼び出しを1回に減らしただけで失敗します。改善したのに赤くなるわけです。検証したかったのは割引後の金額のはずなので、そこだけを見れば十分です。
def test_apply_discount_returns_discounted_price(): repo = StubRepository({1: {"price": 1000}}) service = PriceService(repo)
result = service.apply_discount(item_id=1, rate=0.2)
assert result == 800モック ではなく を使い、決まった値を返すだけにしています。呼ばれ方を検証していないので、内部をどう整理しても通ります。モックを使うべきなのは、「呼ばれたこと自体が仕様」である場合 ── 通知が飛ぶ、監査ログが残る、課金が発生する ── に限られます。
外部の世界との境界にだけ置く。ネットワーク、時刻、乱数、ファイル、決済。ここは制御できないので差し替えが要ります。逆に、自分たちのコード同士の間にモックが増えているなら、粒度が細かすぎるサインです。
4. AI に書かせるときの指示の与え方
以上を踏まえると、指示に含めるべき情報が見えてきます。単位をどう取るかと、何を差し替えてよいかの2つです。これを言わないと、関数単位・モック多用の形に寄ります。
- 検証したい振る舞いを言葉で先に書く — 「割引後の金額が正しいこと」。関数名ではなく結果で指定する
- モックを置いてよい境界を明示する — 「外部APIと現在時刻のみ差し替え可。それ以外は本物を使う」
- の対象を指定する — 「戻り値の金額を検証。呼び出し回数は検証しない」
- を最小に保つよう指示する — 「そのテストに必要な項目だけ用意する」
書かせたあとに読む工程は省けません。ここは Claude Code 受託開発記 EP.09「レビュー文化」 と同じ話で、生成物の量が増えるほどレビューの設計が効いてきます。テストの場合、特に見るべきは アサーション が実質的な検証をしているかの一点です。
5. 弱いアサーションを見分ける
自動生成されたテストで最も多い問題が、実行はされるが何も検証していないというものです。 は上がるので、数値だけ見ると健全に見えます。
| よくある弱い検証 | 何が問題か | どう直すか |
|---|---|---|
| 例外が出ないことだけ確認 | 壊れた値を返しても通る | 戻り値そのものを検証する |
| 戻り値が None でないことだけ確認 | 型が合っていれば通る | 期待する値と比較する |
| ログが出たことだけ確認 | 処理結果を見ていない | 状態の変化を検証する |
| 長さだけ確認 | 中身が入れ替わっても通る | 要素の内容まで比較する |
これらを機械的に見つける手段として があります。コードにわざと小さな変更を加え、テストが落ちるかを確かめる手法です。落ちないなら、そのテストはその部分を守っていないと分かります。実行コストが高いので常時は回しませんが、重要な経路に一度かけると弱い箇所がはっきりします。
6. 粒度を決めるのは人間の仕事
この回の結論はひとつです。書く作業は任せられるが、単位の定義は任せられない。単位の取り方は、そのコードが事業の中でどういう役割を持つか、どの変更が起きうるかという文脈に依存します。文脈は指示として与えるしかありません。
逆に言えば、一度粒度の方針を決めてしまえば、その後の量産は非常に速いということでもあります。方針をプロジェクトの規約として書き残し、指示に含める。これが現時点で最も費用対効果の高いやり方だと考えています。
単位は関数ではなく観測できる振る舞いで取る。モック は外部との境界にだけ置き、内部には スタブ で足りる。自動生成されたテストは アサーション の強さだけは人間が読む。次回は、増えたテストの実行時間をどう抑えるかを扱います。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。