はじめに
この記事では、ソフトウェアテストの指標である、テストカバレッジ(コードカバレッジ)とテスト密度の違いを、送料計算のコードを例に整理します。カバレッジは製品コードのうちテストが実行した部分の割合、密度は規模あたりのテストケース数で、計算に使う値が異なります。
開発環境
- Node.js 24.19.0(Active LTS)
- TypeScript 7.0
- Vitest 4.1.10(カバレッジ計測は
@vitest/coverage-v84.1.10) - Windows 11 / VSCode
題材:送料計算
仕様は次のとおりです。
| 買い物の条件 | 送料 |
|---|---|
| 5000円以上 | 0円 |
| 会員(金額を問わない) | 0円 |
| 非会員で2000円以上5000円未満 | 300円 |
| 非会員で2000円未満 | 600円 |
実装します。
export const calcShippingFee = (subtotal: number, isMember: boolean): number => {
if (subtotal >= 5000 || isMember) return 0; // (a)
if (subtotal >= 2000) return 300; // (b)
return 600; // (c)
};
カバレッジ:テストが通った箇所の割合
カバレッジは、製品コードを小さな単位に分解して、テストの実行中に通った単位の割合を出した値です。
カバレッジ = 通った単位の数 ÷ 単位の総数 × 100
数える単位ごとに別の数字になる
単位の切り方が何種類かあります。Vitestの出力に並んでいる4つの列がそれです。
npx vitest run --coverage
| Vitestの列 | 日本語の呼び方 | 数える単位 | 題材コードの分母 |
|---|---|---|---|
Stmts |
命令網羅(C0) | 実行文 | 2つの if と3つの return で5 |
Branch |
分岐網羅(C1) |
if の判定が真の場合と偽の場合 |
if 2個 × 2 = 4 |
Funcs |
関数網羅 | 関数の呼び出し | 1 |
Lines |
行網羅 | 実行可能な行 | 4 |
| 列なし | 条件網羅(C2) | 条件式の各項が真の場合と偽の場合 | 項3つ × 2 = 6 |
「カバレッジ80%」と言うとき、たいていこの表のどれか1列を指しています。閾値も列ごとに設定します。
coverage: {
thresholds: { statements: 80, branches: 70, functions: 80, lines: 80 },
}
分子と分母はどちらも製品コード側の単位で数えます。テストの件数は式に出てきません。分子が増えるのは、実行してまだ通っていない単位に到達したときです。テスト1件でも100%になりますし、100件でも50%のままのことがあります。
分岐網羅と条件網羅の違い
差が出るのは (a) のように条件が || や && でつながっている箇所です。
3つの入力で比べます。
| 入力 | subtotal >= 5000 |
isMember |
if 全体 |
C1で新しく到達する判定 | C2で新しく到達する項 |
|---|---|---|---|---|---|
| (5000, false) | 真 | 偽 | 真 | 真の場合 |
subtotal >= 5000 の真 |
| (1000, false) | 偽 | 偽 | 偽 | 偽の場合 |
subtotal >= 5000 の偽、isMember の偽 |
| (1000, true) | 偽 | 真 | 真 | なし |
isMember の真 |
上2件で (a) のC1は100%になります。会員だから無料になる経路を一度も実行しないまま100%に届く点が、C2との違いです。
|| は左が真なら右を評価しないので、(5000, true) を実行しても isMember の真は記録されません。C2は主要なツールの標準出力に列がないので、複合条件の箇所は上のような表を書いて確認することになります。
密度:規模あたり何件あるか
密度は件数を規模で割った値です。
テストケース密度 = テストケース数 ÷ 規模
バグ密度 = 検出バグ数 ÷ 規模
規模は対象の大きさを表す数値で、実務では次のどれかを使います。
| 規模の取り方 | 密度の単位 | 使う場面 |
|---|---|---|
| ソースコードの行数 | 件/KLOC | 行数を機械的に数えられるとき |
| 機能数、画面数、API数 | 件/機能 | 設計書ベースで見積もり段階から数えたいとき |
| ファンクションポイント | 件/FP | 言語をまたいで比較したいとき |
KLOC は kilo lines of code の略で、ソースコード1000行を1KLOCと数えます。500行なら0.5KLOCです。空行やコメント、テストコード自身を含めるかは組織ごとに決めておかないと、同じコードでも数字が変わります。
400行のモジュールにテストが80件ある場合は次のようになります。
80件 ÷ 0.4KLOC = 200件/KLOC
分子はテストの件数、分母は製品コードの規模です。テストを実行しなくても両方数えられます。
2つの指標が見ている場所
カバレッジが答えるのは「まだ実行していない if の判定はどれか」です。密度が答えるのは「このモジュールに他と比べて何件書かれているか」です。
分岐網羅100%でも検証されていないもの
describe("calcShippingFee", () => {
it("5000円以上は無料", () => {
expect(calcShippingFee(5000, false)).toBe(0);
});
it("2000円以上は300円", () => {
expect(calcShippingFee(2000, false)).toBe(300);
});
it("2000円未満は600円", () => {
expect(calcShippingFee(1000, false)).toBe(600);
});
});
この3件で4つの判定すべてに到達します。
if と判定結果 |
到達させたテスト |
|---|---|
| (a) の判定が真 | 1件目 |
| (a) の判定が偽 | 2件目、3件目 |
| (b) の判定が真 | 2件目 |
| (b) の判定が偽 | 3件目 |
# % Stmts 100 | % Branch 100 | % Funcs 100 | % Lines 100
ただし3件はすべて isMember が false です。C2で見ると isMember の真だけが空いていて、次の点をどのケースでも確かめていません。
- 会員だから無料になる経路
- 境界値の4999円と5000円、1999円と2000円の切り替わり
- 会員かつ2000円以上という組み合わせ
たとえば (a) を subtotal >= 5000 || (isMember && subtotal >= 3000) と書き間違えたとします。仕様違反ですが、3件はすべて非会員なので全部パスします。判定を実行したかどうかしか数えていないので、条件式の書き間違いはカバレッジに現れません。
密度が高くてもカバレッジは上がらない
it.each([1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900])(
"非会員の買い物 %i 円は送料600円",
(subtotal) => {
expect(calcShippingFee(subtotal, false)).toBe(600);
}
);
件数は10件で、先ほどの3倍以上です。密度も同じ倍率で上がります。ところが到達する判定は4つのうち2つです。
if と判定結果 |
到達 |
|---|---|
| (a) の判定が真 | 未到達 |
| (a) の判定が偽 | 到達 |
| (b) の判定が真 | 未到達 |
| (b) の判定が偽 | 到達 |
パラメータだけ変えたケースを並べても、通る経路は増えません。
密度は分母の定義で数字が変わる
- 分母を行数にすると、冗長に書かれた箇所ほど密度が下がる。AIエージェントが生成したコードは行数が膨らみやすく、テストの量が同じでも数字が悪化する
- 分子の数え方も統一されていない。上の
it.eachを1件と数えるか10件と数えるかで、密度は10倍違う - 絶対値の合否判定には向かない。モジュール間や過去プロジェクトとの比較で、極端に低い箇所を見つけるために使う
バグ密度も同じです。数字が低いとき、品質が高いのかテストが甘いのかは判別できません。テストケース密度とカバレッジを並べて、はじめて解釈できます。
| カバレッジが低い | カバレッジが高い | |
|---|---|---|
| 密度が低い | 単純にテスト不足 | 経路は通っているが、アサーションが不足している可能性 |
| 密度が高い | 同じ経路に重複したテストがある | テストは足りている(重複が多い可能性は残る) |
カバレッジはアサーションを見ていない
it("例外を投げずに動く", () => {
calcShippingFee(5000, false);
calcShippingFee(2000, false);
calcShippingFee(1000, false);
});
アサーションはゼロですが、命令網羅と分岐網羅は100%です。計測しているのは実行された行と判定なので、結果を検証しているかどうかは数字に現れません。密度も件数を数えるだけなので、こちらも検出できません。
検証の強さを見たい場合は、製品コードを機械的に書き換えてテストが落ちるかを試すミューテーションテストを使います。|| を && に入れ替える変異が用意されているので、先ほどの書き間違いも検出できます。
カバレッジ100%が保証しないことと、その担保手段
100%が意味するのは「分母に数えた単位を1回は実行した」ことだけです。それ以外は別の手段を割り当てます。
| 保証されないもの | 題材コードでの例 | 担保する手段の例 |
|---|---|---|
| 仕様の抜け | 会員無料の要件がコードに無ければ、分母に入らないので測れない | 仕様を条件と結果の表にしてからケースを作る。仕様とテストの対応をレビューで突き合わせる |
| 境界値 | 4999円と5000円の切り替わり | 同値分割と境界値分析でケースを設計する |
| 入力の組み合わせ | 会員かつ2000円以上 | ペアワイズでケースを生成する(PICT) |
| 想定外の入力 | 0円、負数、NaN
|
型と入力検証で受け付けない形にする。プロパティベーステストで値を自動生成する(fast-check) |
| 期待値の誤り |
toBe に仕様と違う値を書いてもテストは通る |
ミューテーションテストで、製品コードを書き換えたときに落ちるか試す(Stryker) |
| 条件網羅の不足 |
|| の中の isMember が一度も真にならない |
条件式ごとに真偽の表を作る。Strykerの論理演算子の変異でも検出できる |
| コード外の制約 | データ量、処理時間、同時実行、外部APIの応答 | 性能テストと負荷テスト、本番相当データでの検証、段階リリースと監視 |
全部を毎回やるものではなく、機能の重要度で選びます。送料計算のように金額が出る箇所は境界値と組み合わせまで、表示だけの箇所はカバレッジで実行を確認する程度、という配分になります。
使い分け
カバレッジ:実行していない箇所を見つける
Martin Fowlerは、カバレッジはテストされていない箇所を見つけるための道具であって、テストの良し悪しを表す数値としては役に立たない、と書いています。ある水準を目標に設定すると人はその数値を達成しようとしますが、中身の薄いテストでも高い数値には届きます。
Googleの記事にも理想の数値は存在しないとあり、社内の目安として60%を許容、75%を推奨、90%を模範としたうえで、全社一律の強制は避ける方針が示されています。プロジェクト全体で90%を95%に上げる作業は費用対効果が落ちる一方、コミット単位なら90%を下限に置ける、という使い分けです。割合そのものより、実行されていない行を人が見てリスクを判断するほうが意味がある、とも書かれています。
CIの閾値は「下がったことに気づくための下限」であって、目的ではありません。変更した部分だけを対象にすると、既存コードの割合に引きずられずに新しいコードの抜けを見つけられます。
密度:件数が相場から外れていないかを見る
密度にも基準値はあります。IPAの「ソフトウェア開発分析データ集2022」には、結合テストと総合テストのテストケース密度と検出バグ密度が、P25・中央値・P75で載っています。P25は25パーセンタイルで、各プロジェクトの実績値を小さい順に並べたとき、下から25%の位置にある値です。中央値が50%、P75が75%の位置です。表に入るのは割合ではなく、件/KSLOCの実数です。
| 工程 | 指標 | P25 | 中央値 | P75 |
|---|---|---|---|---|
| 結合テスト | テストケース密度 | 22.2 | 51.7 | 120.1 |
| 結合テスト | 検出バグ密度(現象) | 0.468 | 1.111 | 2.402 |
| 総合テスト | テストケース密度 | 6.4 | 16.0 | 40.7 |
| 総合テスト | 検出バグ密度(現象) | 0.051 | 0.197 | 0.639 |
全開発種別・SLOC規模の値で、単位は件/KSLOCです。実務ではP25を下限、中央値を標準、P75を上限として扱う運用が広く使われています。テスト密度を横軸、バグ密度を縦軸に取り、上下限で3分割して9領域に当てはめるゾーン分析もあります。両方低い領域が最も危険で、報告資料の上では静かに見えても、テストが足りずバグが見つかっていないだけの可能性があります。
ただし、CIで自動判定する用途には次の事情があります。
| 事情 | 内容 |
|---|---|
| 対象工程 | IPAの基準値は結合テストと総合テストのみで、単体テストの基準値は公開されていない |
| ケース数の振れ | パラメータ化テストで1メソッドから数十ケースが生成され、書き方しだいで何倍にも変わる |
| 分布 | 結合テストの検出バグ密度は中央値1.111に対して平均22.866。平均値を基準にすると大半が異常判定になる |
| 更新 | データ集2022が最終版で、今後の発行予定はない |
密度の基準値は、テスト完了前後に人が見て「このモジュールは相場から外れている」と気づくための材料です。CIで自動テストのケース数を機械的に判定する使い方には向きません。
まとめ
カバレッジと密度は、分子、分母ともに値が異なります。
| カバレッジ | 密度 | |
|---|---|---|
| 分子 | 実行中に通った単位の数 | テストケースの件数 |
| 分母 | 製品コードを分解した単位の総数 | 製品コードの規模(行数、機能数など) |
| 分かること | まだ実行していない if の判定はどこか |
他のモジュールと比べてテストケースが多いか少ないか |
| 測るのに必要なもの | テストの実行 | 件数と規模の集計だけ |
| 主な使い道 | 実行していない箇所を探す。CIでは下限として設定する | 件数が相場から外れていないかを人が判断する |
数字が一方だけ高くなる状況は簡単に作れます。分岐網羅が100%でも || の中の条件を実行していないことがありますし、テストケースを3倍にしても通る経路が同じこともあります。テストの抜けを探すときはカバレッジ、テストケースが少ない箇所を探すときは密度、と用途を分けて使うのがよいと思います。