CRAP メトリクス: 複雑度とカバレッジを 1 つの数に
CRAP は Change Risk Anti-Patterns の略です。メソッドの絡まり具合と、テストされていない度合いを掛け合わせ、その結果は単一指標では答えられない問いに答えます。どのコードを変更するのが危険なのか、という問いです。
意図的に粗い道具
Alberto Savoia と Bob Evans は 2007 年、ビルド内の全メソッドを採点する Java 製ツール Crap4j とともに CRAP を発表しました。前提は、複雑度もカバレッジも単独ではあまり意味を持たないということです。込み入ったメソッドでも十分にテストされていれば扱えます。単純なメソッドがテストされていなくても問題になりません。本当に痛いのは、誰もカバーしていない複雑度です。そこでは小さな修正が、誰も捕まえられない驚きを生みます。CRAP は両方の項を 1 つの式に入れ、危険な組み合わせだけが悪いスコアになるようにします。
この名前は意図的に働いています。Change Risk Anti-Patterns という名前の指標は一度話題になるだけですが、「このメソッドはくそだ」と言ってくる指標は直されます。
式と、指数が不均等な理由
複雑度は 2 乗、未カバー率は 3 乗。この非対称性が設計のすべてです。
- • CC はそのメソッドの循環的複雑度、つまり独立した経路の数
- • cov はそのメソッドのカバレッジで 0 から 1 の割合。したがって (1 − cov) が未テストの部分
- • 未テスト部分を 3 乗するため、カバレッジが上がると第 1 項が急速に崩れる。醜いメソッドをテストする見返りが即座に出る
- • 完全カバレッジでは第 1 項がゼロになり、CRAP は CC に等しくなる。テストでは取り除けない残余リスクである
- • 末尾の + CC があるおかげで、この指標が複雑度を無料だと装うことはない
数字が実際に要求するもの
慣例のしきい値 30 を使うと、各複雑度が線の下に入るために必要なカバレッジは次のようになります。
| CC | 0% cov | 50% cov | 100% cov | To clear 30 |
|---|---|---|---|---|
| 5 | 30.0 | 8.1 | 5 | 不要。単純なコードは未テストでも通る。 |
| 10 | 110.0 | 22.5 | 10 | 約 42% |
| 15 | 240.0 | 43.1 | 15 | 約 60% |
| 20 | 410.0 | 70.0 | 20 | 約 71% |
| 25 | 650.0 | 103.1 | 25 | ちょうど 80% |
| 30 | 930.0 | 142.5 | 30 | 100%。しかも線の上にぴったり乗る |
| 31+ | 961.0 | 155.2 | 31 | 到達不能。テストでは救えない。 |
この曲線が語っていること
複雑度 30 を超えるとカバレッジは効かない
しきい値 30 のもとでは、複雑度 31 のメソッドはカバレッジ 100% でも線を超えます。式の欠陥ではなく、これがメッセージです。残る手はメソッドを分割することだけです。
完全カバレッジなら CRAP は複雑度そのもの
テストが複雑度を許すことは決してなく、増幅を止めるだけです。よくカバーされた複雑なメソッドは、その複雑度を隠れたリスクではなく、認識され管理されたリスクとして抱えます。
単純なコードは意図的に放置される
複雑度 5 のメソッドはテストがゼロでもちょうど 30 に位置します。この指標は getter や mapper への無駄な作業を生むのではなく、絡まったコードへ注意を向けます。
弱い足はカバレッジ
cov が測るのは実行であり、検証ではありません。アサーションのないテスト群はカバレッジを上げ CRAP を下げますが、実際のリスクは何も変わりません。まさにその穴を埋めるために、ミューテーションテストが存在します。
実際に使う
式はコード 2 行です。新しいエコシステムで何度も再実装され続ける理由の大半はそれです。
指標そのもの
js任意のカバレッジレポートと任意の複雑度ツールがあれば、式に必要なものはすべて揃います。
// crap.js — coverage is a fraction, 0..1
export function crap(complexity, coverage) {
const untested = 1 - coverage;
return complexity ** 2 * untested ** 3 + complexity;
}
crap(15, 0); // 240
crap(15, 0.5); // 43.125
crap(15, 1); // 15 コードベースではなく変更をゲートする
sh変更されたメソッドにラチェットをかければ、誰も予算をつけていない片付けプロジェクトを開かずに新しいリスクを止められます。既存のスコアはビルドを止めるのではなく、ダッシュボードで見えるままにします。
# CI: score only the methods this branch touched
git diff --name-only origin/main... -- '*.js' \
| xargs node ./tools/crap-report.mjs --threshold 30 --changed-only
# exit non-zero when a touched method crosses the line;
# print the untouched offenders as a report, not a failure スコアが求めているリファクタリング
jsスコアがカバレッジ不足ではなく複雑度から来ているなら、もっとテストするのは誤った応答です。各分岐を複雑度 1 の何かに引き出せば、規則がいくつ増えても駆動部分は平らなままです。
// Before: complexity climbs with every rule anyone adds.
function validate(order) {
if (!order.id) return 'missing id';
if (order.items.length === 0) return 'no items';
if (order.total < 0) return 'negative total';
if (order.currency !== 'USD' && order.currency !== 'EUR') return 'bad currency';
if (order.customer && !order.customer.email) return 'customer without email';
return null;
}
// After: each rule is trivially testable, the driver stays at 2.
const RULES = [
[(o) => !o.id, 'missing id'],
[(o) => o.items.length === 0, 'no items'],
[(o) => o.total < 0, 'negative total'],
[(o) => !['USD', 'EUR'].includes(o.currency), 'bad currency'],
[(o) => Boolean(o.customer) && !o.customer.email, 'customer without email']
];
function validate(order) {
for (const [fails, message] of RULES) if (fails(order)) return message;
return null;
} 全員を苛立たせずに使う方法
2 つのレバーを別々に読む
- - 複雑度が押し上げた高スコアはリファクタリングの仕事であり、テストの仕事ではない
- - カバレッジ不足が押し上げた高スコアはテストの仕事であり、しかも安い
- - スコアの隣に必ず CC とカバレッジを出す。数値だけではどちらなのか分からない
変更頻度で重み付けする
- - 変更リスクが問題になるのは、変更が起きる場所だけ
- - 4 年間触られていないファイルのスコア 200 は、毎週編集されるファイルの 60 より緊急度が低い
- - スコア × コミット頻度で並べ、その上位から片付ける
除外を明示する
- - 生成コード、アダプタ、網羅的な switch は、実際のリスクを増やさずに複雑度を膨らませる
- - しきい値を黙って上げるのではなく、理由をコメントに書いて設定で除外する
- - 守っていたコードが生成されなくなったら、除外リストを見直す
よくある誤読
品質スコアとして扱う
CRAP が見積もるのはコードを変更するリスクです。コードが正しいか、命名が良いか、設計が良いかは何も語りません。本質的な業務複雑度を持つきれいでよくカバーされたメソッドも、不快なメソッドと同じスコアになります。
カバレッジで細工する
何でもスナップショットするテストや、アサーションのない通し実行は、数値を動かすだけでリスクを動かしません。CRAP をゲートにするなら、レビューかミューテーションテストか、その両方でテストを誠実に保つ何かが必要です。
片付けの大型プロジェクトを立ち上げる
レガシーコードにリポジトリ全体の CRAP レポートを当てると、数値が大きすぎて無視されます。代わりに変更されたコードにラチェットをかけ、危険な部分はもともと触る予定だった人に直させましょう。
保守されたツールを期待する
元の Crap4j は長年休眠しています。.NET では NDepend がこの指標を持ち、Rust、.NET、Groovy にはコミュニティ実装がありますが、多くのスタックではすでに集めているデータから自分で計算します。
1 つの数、1 つの問い
CRAP は品質スコアではなく、そう意図されたこともありません。答えるのはもっと狭く、もっと有用な問いです。来週誰かがこのメソッドを編集したら、静かに何かが壊れる確率はどれくらいか。複雑度が「間違え方が何通りあるか」を、カバレッジが「そのうち何通りを誰かが見ているか」を語り、式は後者を前者より重く扱います。
それだけでビルドに組み込む価値があります。新規および変更されたコードをしきい値でゲートし、残りはスコアと変更頻度で並べ、2 つの入力を別々に読んで、数値が「このメソッドを分割する」か「テストする」という行動に解けるようにしてください。ただし、式のカバレッジ側が意味するのは、あなたのアサーションが意味させた分だけです。
関連するエンジニアリング記事
この式のカバレッジ側こそがミューテーションテストの追及対象であり、エージェントのワークフローでは既に専用のロールを与えられています。
よくある質問
CRAP のスコアはどこから悪いのですか?
30 が慣例的な既定しきい値で、Crap4j から受け継がれ、後続の実装の多くがそのまま使っています。線を守る負担が小さい新規コードでは下げ、レガシーコードではラチェットを進めながら維持してください。レポートの見栄えのために上げるのは避けます。
なぜ複雑度は 2 乗で、未テスト率は 3 乗なのですか?
カバレッジのほうが複雑度より速くスコアを動かすためです。3 乗の項はカバレッジが完全に近づくとゼロへ崩れ、複雑なコードをテストする行為に即座の見返りを与えます。一方、2 乗の項と末尾の複雑度は、どれだけテストしても消えない床を残します。
CRAP メトリクスは今も有効ですか?
元の Java ツールは長く休眠していますが、式は新しいエコシステムに繰り返し現れます。実装が些細である一方、単一指標のダッシュボードでは答えられない問いに答えるからです。カバレッジと複雑度をすでに集めているなら、あと 2 行で手に入ります。
CRAP が低ければ変更しても安全ですか?
複雑度がカバーされているという意味で、よくテストされているという意味ではありません。カバレッジは検証ではなく実行を数えるため、弱いテストの上に建った低スコアは、居心地の悪い現実の上に置かれた居心地のよい数値です。カバレッジが本物かを確かめる標準的な手段はミューテーションテストです。
CRAP でビルドを失敗させるべきですか?
新規および変更されたメソッドへのラチェットとしては、はい。強制でき、誰も予定を組む必要がありません。既存のコードベース全体に対するゲートとしては、いいえ。それは行動の変化ではなくバックログを生みます。