テストの質をどう測るか — カバレッジとミューテーションテスト
カバレッジは「そのコードは実行されたか」しか教えてくれず、「実行結果が正しく検証されたか」には答えない。行・分岐・MC/DCという強度の異なるカバレッジ種別、Goodhartの法則が生む100%神話という罠、航空機ソフトウェアDO-178Cが要求するMC/DCを辿ったうえで、コードに小さなバグを実際に仕込みテストが検知できるかを測る「ミューテーションテスト」が、カバレッジの盲点をどう補うかを見る。
カバレッジという安心感の罠
カバレッジ(coverage)は、テスト実行時にソースコードのどれだけが実際に通過したかを測る指標である。「テストを書いた」という主観的な安心感を数値という客観的な形で可視化できる点が支持される理由だが、同時に「数値さえ高ければ品質が高い」という誤解を招きやすい指標でもある。カバレッジが答えられるのは「そのコードは実行されたか」だけである。「実行された結果が正しく検証されたか」には一切答えない。この非対称性を理解しないままカバレッジ目標を運用すると、しばしば数字だけが上がって品質は上がらないという逆説的な結果を招く。
カバレッジの種類
カバレッジには強度の異なる複数の種類があり、下に行くほど要求される網羅性が厳しくなる。
| 種別 | 何を測るか | 備考 |
|---|---|---|
| 命令/行カバレッジ | 実行可能な文・行が少なくとも1回実行されたか | 最も緩く、最も一般的な指標 |
| 分岐カバレッジ | if・switchなどの各分岐が少なくとも1回通過したか |
行カバレッジより厳しい。未到達のelseを検出できる |
| 条件カバレッジ | 論理式内の各部分条件が真・偽の両方を取ったか | 分岐全体の結果は問わないため、単独では弱い場合がある |
| MC/DC(条件分岐網羅) | 各条件が他条件を固定したまま単独で分岐結果を変えうることを示す | N個の条件に対しおよそN+1件のテストで済む。航空業界標準で要求される |
| パスカバレッジ | 制御フロー上のすべての実行経路を網羅する | ループがあると経路数が指数的に爆発し、現実的にはほぼ達成不能 |
これらは包含関係にあり、MC/DCを満たせば分岐カバレッジも自動的に満たされるが、逆は成立しない。
MC/DCがテスト数を抑えられる理由
3つの条件A・B・Cからなる判定A && B && Cを考える。すべての組み合わせを網羅する複合条件網羅(MCC) は2^3 = 8通りのテストを要求するが、MC/DCは「各条件が単独で判定結果を変えうることを、他の条件を固定した状態で示す」ことだけを要求するため、条件数Nに対しおよそN+1件(このケースでは4件程度)で足りる。条件数が10個であれば、MCCは1024通り、MC/DCはおよそ11件で済む。DO-178Cのような安全クリティカル規格が理論上最も厳密なパスカバレッジやMCCではなくMC/DCを採用しているのは、この現実的なテスト数と検証コストのバランスによる。
主要ツール
| ツール | 対象言語 | 特徴 |
|---|---|---|
| gcov / lcov | C / C++ | gcovはGCC付属の計装ツール、lcovがHTMLレポートを生成する定番の組み合わせ |
| Istanbul / nyc | JavaScript / TypeScript | Istanbulがコード計装エンジン、JestのカバレッジもIstanbul系の実装を利用 |
| JaCoCo | Java | バイトコード計装方式。Maven/Gradleと統合する |
| coverage.py | Python | pytest-cov経由でpytestと連携するのが定番 |
coverage.pyをpytest-cov経由で実行すると、次のようなレポートが得られる(数値は例示)。
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------
orders/pricing.py 48 6 12 3 82%
orders/discount.py 30 0 8 0 100%
------------------------------------------------------
TOTAL 78 6 20 3 89%
BrPart列は部分的にしか通過していない分岐(ifは通ったがelseは通っていない等)を示す。行カバレッジだけを見ているとこの欠落に気づけない。
カバレッジの罠
Goodhartの法則——「measureがtargetになった瞬間、そのmeasureは良い指標でなくなる」——はカバレッジ運用に直撃する。「カバレッジ80%以上をマージ条件にする」という規則を敷くと、開発者はその数値を満たすこと自体を目的化し始める。
// 行カバレッジは満たすが、何も検証していないテスト
test("processOrder runs without crashing", () => {
processOrder(sampleOrder); // assertが一つもない
});
100%カバレッジ神話も根強い。100%の行カバレッジは「すべての行が最低1回実行された」ことしか意味せず、アサーションを伴わないテストでも達成できてしまう。カバレッジは「テストが存在しないことを検出する」下限の安全網であって、「テストが正しいことを保証する」上限の証明ではない。
次の関数を考える。
def apply_discount(price, is_member):
if is_member:
price = price * 0.9
return price
apply_discount(1000, True)という1件のテストだけで、ifブロックを含む全行が実行され行カバレッジは100%に達する。しかしis_member=Falseの経路は一度も通っておらず、分岐カバレッジで見て初めてこの欠落が可視化される。さらに構造的な欠落は0%から70〜80%までは安価に見つかるが、80%から100%に近づくほど対象は些末なgetter/setterや到達困難な防御的エラーハンドリングになりがちで、得られる安全性の向上は急速に逓減する。
安全クリティカル分野でのカバレッジ要求
DO-178C(航空機搭載ソフトウェアの認証ガイドライン、RTCA/EUROCAE策定)は、故障の重篤度に応じて設計保証レベル(DAL)をAからEまで区分し、レベルごとに要求される構造カバレッジを定めている。
| DALレベル | 想定される故障影響 | 要求される構造カバレッジ |
|---|---|---|
| レベルA | 機体の継続安全飛行・着陸を妨げうる致命的な故障 | MC/DC |
| レベルB | 重大な故障 | 分岐カバレッジ |
| レベルC | 軽微な故障 | 命令カバレッジ |
| レベルD/E | 些細/安全性に無関係 | 明示的要求なし/対象外 |
ISO 26262(自動車の機能安全規格)も自動車安全度水準(ASIL)に応じてカバレッジの推奨度を変える。ASIL A/Bでは命令カバレッジ、より高いASIL C/DではMC/DCの充足が求められる。DO-178Cが「必須要求」として明記するのに対し、ISO 26262は「推奨」段階の格付けが多く、硬い義務規定というよりガイドラインに近い。これらの規格は、カバレッジを「品質の証明」としてではなく「テストされていない危険なコードが残っていないことの下限保証」として位置づけている。
健全な目標設定の考え方
- 差分カバレッジ: 「新規・変更行のカバレッジ」を閾値にし、既存の低カバレッジ資産を無理に押し上げず「これ以上悪化させない歯止め」として機能させる
- リスクベースの濃淡: 決済・認証のような影響の大きいモジュールは高い基準を課し、低リスク領域は緩める
- がん検診の比喩: 低カバレッジは「精査すべき箇所がある」というシグナルであって、高カバレッジがそのまま「健康」を意味するわけではない
- アサーションの中身はレビューで見る: ツールでは検出できない空疎なテストは人間が見抜くしかない
これらの限界を補う技法が、カバレッジの「量」ではなく検証の「質」を直接測るミューテーションテストである。
ミューテーションテストの登場
ミューテーションテストは、ソースコードを機械的に少しだけ変異させた「ミュータント(mutant)」を大量に生成し、既存のテストスイートがその変異を検出できるかを一つずつ確認する技法である。検出できればそのミュータントは「殺された(killed)」、できなければ「生存した(survived)」と判定される。発想の原点は単純な問いにある——「行カバレッジが100%でも、そのテストは本当にバグを見つけられるのか」。
理論の起源は古く、Richard Liptonが学生時代の1971年に着想したとされ、Richard A. DeMillo、Richard J. Lipton、Frederick G. Saywardによる1978年の論文“Hints on Test Data Selection”で体系化された。この論文は2つの仮説を提示する。
- 有能なプログラマ仮説: 熟練したプログラマのバグは、単純な構文変更で表現できる小さなものが大半である
- カップリング効果: 単純な変異を検出できるテストは、それらが組み合わさった複雑な欠陥も暗黙に検出できる
ミューテーション演算子の例
| カテゴリ | 変異の例 |
|---|---|
| 条件反転 | if (a == b) → if (a != b) |
| 境界値変更 | a < b → a <= b |
| 算術演算子置換 | a + b → a - b |
| 論理演算子置換 | a && b → a || b |
| 戻り値変更 | return true; → return false; |
| 文の削除 | 1文まるごと削除 |
public boolean isAdult(int age) {
return age >= 18;
}
境界値変更演算子はage >= 18をage > 18に変異させる。テストがisAdult(18)を検証していなければ、このミュータントは生存する。行カバレッジだけでは決して見抜けない種類のテストの穴が、ここで可視化される。
等価ミュータント問題と計算コスト
構文は変わっているが、あらゆる入力に対して元のプログラムと全く同じ振る舞いをする「等価ミュータント」は原理的に決して殺せない。ある変異が等価かどうかを一般的に自動判定することは決定不能であり、実務では生存ミュータントを人手でレビューし切り分ける作業が発生する。
ミューテーションテストの素朴な実装は「ミュータント数 × テストスイート実行時間」のコストがかかる。実務ツールは次のような最適化でこれを緩和している。
- カバレッジ優先実行: そのミュータント箇所に行カバレッジを持つテストだけを回す。JavaのPIT(Pitest)が採用
- Mutation Switching: 複数のミュータントを1つのバイナリにまとめ実行時フラグで切り替える。Stryker.NETやstryker4sが採用
- 選択的ミューテーション: 経験的に効果の高い一部の演算子だけに絞る
- 差分ベース: 変更された行だけを対象にする。Googleは“State of Mutation Testing at Google”で、コードレビュー時に差分へミューテーションを適用しレビュアーに提示する仕組みを報告している
- 並列実行: ミュータントごとの実行は独立しているためCIワーカーを増やして並列化しやすい
主要ツールとミューテーションスコアの計算例
| ツール | 対象言語 | 特徴 |
|---|---|---|
| PIT(Pitest) | Java | バイトコードレベルで変異を注入。Java界の事実上の標準 |
| Stryker | JavaScript/TypeScript, C#, Scala | Mutation Switchingで高速化 |
| mutmut | Python | ソースの整形を保ったまま変異を適用 |
ミューテーションスコアは「殺されたミュータント数 ÷(全ミュータント数 − 等価ミュータント数)」で算出される。あるモジュールで100個のミュータントを生成し、72個が殺され、20個が生存、8個が等価ミュータントと判明した場合、スコアは72 ÷ (100 − 8) = 72 ÷ 92 ≈ 78.3%となる。等価ミュータントを除外せず単純に72 ÷ 100 = 72%とすると、原理的に殺せない分だけスコアが不当に低く出る。
何を検出できないか、実務での運用
ミューテーションテストは万能ではない。「書かれているコード」を変形するものであり、「本来書かれるべきだが書かれていない処理」の欠落は検出できない。競合状態のようなタイミング依存のバグにも弱い。これらはプロパティベーステストのような別の技法が補う領域である。
計算コストの高さゆえ、実務では全コミットへの必須ゲートとして使うより、夜間・週次バッチでの定期実行、差分ベースでPR単位に絞ってレビュアーに提示する運用、決済ロジックなど重要モジュールへの絞り込み、スコアに厳格な閾値を課しすぎない運用が現実的である。
まとめ
カバレッジは「実行の量」を測り、ミューテーションテストは「検証の質」を測る——この対比で整理すると両者の関係が理解しやすい。行・分岐・条件・MC/DC・パスという段階のどれを選ぶかはコストと保証したい強度のトレードオフであり、安全クリティカル分野ではその強度が規格として明文化されている。一般開発では、カバレッジを目的化せず差分カバレッジによる歯止めとリスクベースの濃淡付けを行い、ミューテーションテストによる質的補完を組み合わせることが、Goodhartの法則の罠を避ける現実的な道になる。単体テストの設計原則については良い単体テストとは何かを、次の記事では、テストの信頼性そのものを蝕む「フレーキーテスト」を扱う。
参考文献
- RTCA/EUROCAE, DO-178C “Software Considerations in Airborne Systems and Equipment Certification”
- ISO 26262(Road vehicles — Functional safety)Part 6
- Richard A. DeMillo, Richard J. Lipton, Frederick G. Sayward, “Hints on Test Data Selection: Help for the Practicing Programmer”(1978年、IEEE Computer)
- Henry Coles ほか, “PIT: a practical mutation testing tool for Java”(ISSTA 2016)
- Goran Petrovic, Marko Ivankovic, “State of Mutation Testing at Google”(Google Research)
- Yue Jia, Mark Harman, “An Analysis and Survey of the Development of Mutation Testing”