Claude Codeでカバレッジの穴を埋めるテスト追加の手順 — 未カバー分岐だけ渡す
カバレッジレポートの未カバー行と分岐だけをClaude Codeに渡し、不足分のテストを足す手順です。coverage.pyの出力例、c8とJaCoCoの読み方、数字だけ満たす弱いテストの見分け方を載せます。
Claude Codeに「カバレッジを上げて」とだけ頼むと、数字は上がります。ただし、どの分岐が本当に未検証なのかは見えないまま、似たようなテストが増えがちです。起点をカバレッジレポートに置き、未カバーの行と分岐だけを渡せば、足すテストの数が絞れます。公式のテスト手順にある「テストされていない関数を探す」を、レポートで具体化する流れです。
レポートを渡すと、頼み方がどう変わるか
公式のテスト手順は4段です。テストされていないコードを探し、雛形を作り、エッジ条件のケースを足し、実行して失敗を直します。1段目の「探す」は、ファイル名を渡してClaudeに読み取らせる形で書かれています。
この探索を、カバレッジツールの出力で置き換えるのが本記事の骨子です。ツールは「実行されなかった行」を機械的に数えます。Claudeは、その行がどんな条件で通るかをコードから読み、テストに直します。役割が分かれるので、探索に使うコンテキストも減らせます。
Claudeは雛形を作るとき、既存テストのスタイル、フレームワーク、アサーションの書き方を読んで合わせます。そのため、レポートさえ渡せば、書き方の指示は最小限で済みます。
カバレッジ起点の4段階
- 1
計測する
分岐カバレッジ付きでテストを実行し、未カバー箇所を出力します。
- 2
絞って渡す
100%のファイルを除き、未カバーの行と分岐だけをClaudeに渡します。
- 3
分岐ごとに足す
1つの未カバー分岐に、1つのテストを対応させます。
- 4
弱さを確かめる
コードに壊れた改変を入れ、足したテストが落ちるかを見ます。
coverage.pyのレポートで未カバー分岐を取り出す
Pythonの例で流れを見ます。shipping.pyに、送料を計算する小さな関数を用意しました。
def shipping_fee(weight_kg, region, member):
if weight_kg <= 0:
raise ValueError("weight must be positive")
fee = 500
if weight_kg > 10:
fee += 300
if region == "remote":
fee += 800
if member:
fee = fee // 2
return fee既存のテストは、基本ケースと会員割引の2本だけです。
# test_shipping.py
from shipping import shipping_fee
def test_basic():
assert shipping_fee(1, "local", False) == 500
def test_member():
assert shipping_fee(1, "local", True) == 250分岐カバレッジを付けて実行し、-mで未カバーの位置を出します。--skip-coveredを付けると、100%のファイルは表から外れます。
coverage run --branch -m pytest -q
coverage report -m --skip-coveredcoverage.py 7.17.0とPython 3.12で実行した出力は次のとおりです。
Name Stmts Miss Branch BrPart Cover Missing
---------------------------------------------------------
shipping.py 11 3 8 3 68% 3, 6, 8
---------------------------------------------------------
TOTAL 16 3 8 3 75%
1 file skipped due to complete coverage.Missingの3, 6, 8は、実行されなかった行番号です。3行目は例外の送出、6行目は重量加算、8行目は遠隔地加算にあたります。BrPartが3なので、分岐の片側しか通っていないif文が3つあるとも読めます。
行カバレッジと分岐カバレッジは見える範囲が違う
公式のcoverage.py文書では、分岐カバレッジは「行から行への移動」を数えます。if文が一度も偽にならなければ、全行が実行済みでも「部分的な分岐」として警告されます。Missing列では36->38のように矢印で表示されます。
行カバレッジだけで運用すると、この種の穴は見えません。渡すレポートは、必ず分岐付きにします。
ループの扱いにも注意が要ります。文書の例では、forの対象が空だとループ本体の行がMissingに出るだけで、分岐は報告されません。分岐の未検証は、本体が一度回ったあとに初めて現れます。穴を1つ埋めたら再計測すると、新しい穴が出ることがあります。
JSON出力なら位置を機械的に渡せる
表をそのまま貼るより、coverage jsonで取れる構造化データのほうが、後続の指示が書きやすくなります。
coverage json -o cov.json -q上の例では、ファイルごとのエントリにmissing_linesとmissing_branchesが入り、値は次のとおりでした。
missing_lines : [3, 6, 8]
missing_branches : [[2, 3], [5, 6], [7, 8]][2, 3]は「2行目のifが真になって3行目へ進む」遷移が未実行という意味です。行番号の組なので、プロンプトで「この3遷移をそれぞれ1テストで通す」と数を固定できます。
c8とJaCoCoのレポートを読むときの確認点
言語が違っても、渡し方は同じです。変わるのは、レポートの数え方と「100%を除く」オプションの名前です。
| ツール | 100%のファイルを除く | 閾値の検査 | 分岐の扱い |
|---|---|---|---|
| coverage.py | 100%のファイルを除く--skip-covered | 閾値の検査--fail-under | 分岐の扱い--branchで計測、36->38形式 |
| c8(Node.js) | 100%のファイルを除く--skip-full | 閾値の検査--check-coverageと--branches | 分岐の扱いIstanbul互換のレポーター |
| JaCoCo(Java) | 100%のファイルを除くレポート画面で色分け | 閾値の検査別途設定 | 分岐の扱い部分カバーを黄色のひし形で表示 |
c8が使うV8のカバレッジは、既定ではエンジンが読み込んだファイルしか対象にしません。テストから一度も読み込まれなかったファイルは、集計に入らないということです。未テストのモジュールを探したいときは、--allの説明を先に確かめます。
JaCoCoは、if文とswitch文の分岐を数えます。例外処理は分岐として数えません。したがって、try/catchのcatch側が未テストでも、分岐の穴としては報告されません。catch側は、行カバレッジか自分のレビューで拾います。
また、JaCoCoの行カバレッジは、デバッグ情報つきでコンパイルされたクラスでのみ計算されます。レポートに行の色が付かないときは、まずビルド設定を疑います。
渡し方の型: 未カバー分岐を1つずつ条件に直す
レポートを貼って「テストを足して」と頼むだけでは足りません。Claudeには、行番号を「どんな入力でそこを通るか」に直す作業を、先にさせます。
cov.json の shipping.py の missing_branches を読んで。
遷移ごとに、そこを通る入力の条件を1行で書き出して。
テストはまだ書かないで。条件の一覧が返ったら、人間が目で確かめます。ここで間違いに気づけば、誤ったテストは生まれません。そのうえで、次の指示に進みます。
その条件ごとに test_shipping.py へテストを1本ずつ足して。
既存の test_basic と同じ書き方に合わせる。
期待値は仕様から決めて、関数の出力を写さない。
足したら coverage report -m --skip-covered を再実行して。最後の一文が、検証の入口です。公式のベストプラクティスにも、Claudeに「実行して合否が分かるもの」を持たせると、自分で直して再実行するループが回ると書かれています。数値を出力として返させれば、報告が「通りました」だけで終わりません。
期待値を関数の出力から写させない
カバレッジを満たすだけなら、関数を呼んで戻り値をそのままassertに貼れば足ります。その場合、実装のバグはテスト側にも写ります。指示に「仕様から期待値を決める」と入れたのは、そのためです。仕様が文書にない関数では、人が期待値を一覧で渡します。
100%でも守れていないテストを見分ける
数字だけを満たす弱いテストを、実際に作って比べました。次はアサーションを1つも持たないテストです。
# test_weak.py
import pytest
from shipping import shipping_fee
def test_runs_all_paths():
shipping_fee(11, "remote", True)
shipping_fee(1, "local", False)
with pytest.raises(ValueError):
shipping_fee(0, "local", False)既存の2本とあわせて実行すると、shipping.pyのカバレッジは次のようになりました。
shipping.py 11 0 8 0 100%行も分岐も100%です。ここで本番コードに壊れた改変を入れます。fee += 800をfee -= 800に書き換えたところ、3本とも通りました。遠隔地加算が逆になっても、テストは検出できません。
次に、仕様から期待値を決めた強いテストに差し替えます。
# test_strong.py
import pytest
from shipping import shipping_fee
def test_heavy():
assert shipping_fee(11, "local", False) == 800
def test_remote():
assert shipping_fee(1, "remote", False) == 1300
def test_zero_weight():
with pytest.raises(ValueError):
shipping_fee(0, "local", False)カバレッジは同じ100%です。ところが、同じ改変を入れると5本のうち1本が落ちます。数字は同じでも、検出力が違います。
同じ100%で、改変の検出が変わる
戻り値を見ないテスト
呼び出しだけで行と分岐を通します。fee -= 800への改変でも全件が通りました。
期待値を置いたテスト
仕様から決めた金額をassertします。同じ改変で1件が失敗し、壊れたことが分かります。
この「壊してみる」作業を自動で回す道具がミューテーションテストです。JavaScript向けのStrykerを使った設定例とStop hookでの運用は、Strykerのミューテーションテストで弱いテストを補強する記事にまとめています。本記事のレポート起点は「どこが未実行か」、そちらは「実行された箇所が守られているか」を見る役割です。
手で確かめる場合は、足されたテストのうち1本について、対象の条件式を1つ書き換えてテストを実行します。落ちなければ、そのテストは分岐を通っているだけです。
CLAUDE.mdと許可ルールで毎回の手間を減らす
同じ依頼を繰り返すなら、手順を固定します。公式のベストプラクティスは、CLAUDE.mdに書くべきこととして、リポジトリでの確認方法、つまり正確なテストコマンドと単一ファイルの実行方法を挙げています。カバレッジのコマンドも、ここに入ります。
## テストとカバレッジ
- 全体: `coverage run --branch -m pytest -q`
- 未カバー確認: `coverage report -m --skip-covered`
- テストを足すときは、未カバー分岐1つにつき1テスト
- 期待値は仕様から決める。関数の戻り値を写さない
- 既存テストの書き方(命名・フィクスチャ)に合わせるこの断片は、本記事の例に合わせた書き方の一例です。ベストプラクティスには、CLAUDE.mdを短く保つ方針も書かれています。カバレッジの数値目標のような頻繁に変わる値は、書かずに設定ファイル側の閾値へ任せるほうが保守しやすくなります。
計測コマンドを毎回承認するのが面倒なら、許可ルールで通せます。公式の許可ルールでは、Bash(npm run *)のように、サブコマンドの後ろに*を置く書き方が案内されています。
{
"permissions": {
"allow": [
"Bash(coverage run *)",
"Bash(coverage report *)",
"Bash(coverage json *)"
]
}
}計測コマンドだけを通し、ファイルを書き換える操作は通常どおり承認に回します。
つまずきやすい点
- テストファイルが表に出る: TOTAL行にはテストファイル自身の文も数えられます(上の例の16)。計測対象は
--includeや--omitで絞ります。 - 穴を埋めたのに別の穴が出る: ループの例のとおり、本体が一度回って初めて内部の分岐が現れます。再計測を手順に含めます。
- 到達不能な分岐を無理に通させる: 防御的な分岐は、テストで通せないことがあります。coverage.pyには、意図した部分分岐を除外する
# pragma: no branchがあります。除外は理由とともに人が判断します。 - 余計な修正が混ざる: テスト追加の依頼で本番コードまで書き換えられることがあります。範囲を絞る指示文は、余計な修正とテスト追加を抑える指示文にあります。
まとめ
レポート起点の運用では、Claudeに渡すのは「未カバーの遷移の一覧」で、頼むのは「条件への翻訳」と「遷移ごとの1テスト」です。100%に届いたら、それは出発点にすぎません。壊した改変がテストに検出されるかを1回でも確かめれば、数字と検出力のずれが見えます。新規コードのテストを一から書かせる場面は、単体テストを書かせる手順が扱っています。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。