CRAP メトリクス: 未テストの複雑度を見つける
CRAP は Change Risk Anti-Patterns の略です。メソッドの絡まり具合と、テストされていない度合いを掛け合わせ、単一指標では答えられない問いに答えます。どのコードを変更するのが危険なのか。エージェントがコードを書くようになって、この問いは一気に切実になりました。
意図的に粗い道具
Alberto Savoia と Bob Evans は 2007 年、ビルド内の全メソッドを採点する Java 製ツール Crap4j とともに CRAP を発表しました。前提は、複雑度もカバレッジも単独ではあまり意味を持たないということです。込み入ったメソッドでも十分にテストされていれば扱えます。単純なメソッドがテストされていなくても問題になりません。本当に痛いのは、誰もカバーしていない複雑度です。そこでは小さな修正が、誰も捕まえられない驚きを生みます。CRAP は両方の項を 1 つの式に入れ、危険な組み合わせだけが悪いスコアになるようにします。
この名前は意図的に働いています。Change Risk Anti-Patterns という名前の指標は一度話題になるだけですが、「このメソッドはくそだ」と言ってくる指標は直されます。
実際に何のためにあるのか
CRAP はダッシュボードの飾りではありません。どのスコアも 4 つの判断のどれかに解けます。それが計算する理由のすべてです。
次のテストをどこに置くか
降順に並べたレポートは、テスト工数の作業キューになります。上位は、費やす 1 時間あたりのリスク削減が最大の場所です。全体のカバレッジ率を追いかけるより、はるかに良い答えです。
触る前に何をリファクタリングするか
複雑度由来の高スコアは、そのメソッドが抵抗してくるという警告です。振る舞いを足す前に分割するほうが、足してから結果をテストしようとするより通常は安く済みます。
どの変更を人が丁寧に読むべきか
触ったメソッドの CRAP を上げる diff は、行単位のレビューに値します。下げる diff はざっと読んでよい。この振り分けの判断こそ、この指標がレビュー時間で元を取る場所です。
リリース前に何を回帰テストするか
変更リスクと直近の変更頻度を合わせれば、今回のリリースで回帰パスに値するモジュールと、中で何も動いていないから信用してよいモジュールが分かります。
式と、指数が不均等な理由
複雑度は 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 を下げますが、実際のリスクは何も変わりません。まさにその穴を埋めるためにミューテーションテストがあり、そしてそれは LLM がまっすぐ踏み込む穴でもあります。
QA チームはこれをどう使うか
メトリクス解説でいちばん抜け落ちる部分です。CRAP は照準器であり、それを向けるのは QA です。
回帰スイートを膨らませるのではなく、狙いを定める
- - メソッドを CRAP × コミット頻度で並べ、リリースの回帰パスをそのリストの上から組む
- - CRAP が低く変更もないモジュールは毎リリースの再実行を必要としない。そこから、上位に充てる時間が生まれる
- - 永遠に増えるだけで刈られない静的スイートを維持するのではなく、リリースごとに並べ直す
自動化と探索的テストを切り分ける
- - 高複雑度かつ低カバレッジは単体テストの穴であり、探索的テストの穴ではない。20 分岐を手で辿らせるのは優秀なテスターの浪費
- - 高複雑度かつ高カバレッジこそ探索的テストが効く場所。経路は動いていても要求が間違っている可能性が残る
- - 未定義の振る舞いは複雑なメソッドの未カバー分岐に潜む。だから異常系と境界値はまずそこから設計する
リリースゲートを意見ではなく数字で議論できるようにする
- - 「変更されたメソッドがしきい値超えなし」というゲートは強制でき、レビューでき、スプリント末に反論しにくい
- - スコアの隣に 2 つの入力を出し、直し方を一意にする。分割するか、テストするか
- - 流出した欠陥を、その出所メソッドの CRAP と突き合わせて記録する。この相関が、うちのしきい値をそのまま継ぐのではなく自分たちの値を調整する方法になる
開発との共通言語として使う
- - 「このメソッドは汚い」という品質の異議は納期に負けるが、「このメソッドは複雑度 24 でカバレッジ 30%」なら通常負けない
- - 機能が入る前にリファクタリングを求める、正当で事前合意済みの根拠を QA に与える
- - 開発者を無駄仕事から守る効果もある。この指標は自明なコードにテストは不要だと明言しているからだ
LLM が書いたコードを CRAP で判定する
エージェントはこの指標の経済性を変えました。誰も読み切れない速さでもっともらしいコードを出し、しかも自分を測るテストを喜んで書きます。複雑度とカバレッジの対比は、残された最も安価で正直なシグナルの 1 つです。
具体的な危険は、LLM が「見える目標」を最適化することです。テストを求めればテストが出て、カバレッジを求めればカバレッジが出ます。CRAP の両方の半分はエージェントが動かせますが、正直に動かせるのは片方だけです。複雑度は、それが書いたコードの構造的性質であり、言葉で上げ下げできません。カバレッジは、行を実行するだけで何も主張せずに膨らませられる数字です。だからこの対は診断的になります。複雑度はエージェントが何を作ったかを語り、カバレッジはそれについて何を主張しているかを語り、その差こそ見るべき場所です。
リポジトリではなく diff を採点する
- - エージェントが触ったメソッドの CRAP を前後で算出し、差分をプルリクエストに投稿する
- - 複雑度が上がりカバレッジが横ばいなのは、既存関数に振る舞いをねじ込んだ痕跡であり、エージェントが機能を足すときの既定の形である
- - 複雑度が下がりカバレッジが上がるのが良い実行の姿。レビューを速くすることで報いる価値がある
複雑度の上限をエージェント自身の指示に書く
- - Uncle Bob の群れはまさにこれをしている。cleaner ロールは何よりも先に CRAP ツールを走らせ、複雑度を 6 以下に落とす
- - スコアでゲートするより賢い使い方だ。複雑度 6 では、カバレッジが何をしても数字はほとんど上がれない
- - クリーンコードについての段落ではなく、しきい値とコマンドを与えること。チェックに終了コードがあるからエージェントは従う
ツール出力をループに戻す
- - ダッシュボードの CRAP レポートは何も変えない。同じレポートを失敗チェックとして次のターンに流し込めば直る
- - コードを書いた工程とは別の工程で走らせ、エージェントに自分の宿題を採点させない
- - 再試行に上限を設ける。2 回でしきい値を下回れないエージェントは、もう一度やらせてほしいのではなく設計が間違っていると言っている
1 つのエージェントにカバレッジ上げと指標報告を兼ねさせない
- - カバレッジは細工できる半分であり、自分のスコアを改善せよと言われたエージェントは数字への最短路を見つける
- - すべての CRAP ゲートにミューテーション実行を組ませる。その新しいテストが何かを主張しているのかを問うために
- - スコアについてのエージェントの説明は宣伝文だと思うこと。証拠はツールの出力である
エージェントが書いたコードのシグナルを読む
著者がモデルの場合、同じ数字は具体的で見分けのつく意味を持ちます。
| シグナル | 通常の意味 | 取るべき行動 |
|---|---|---|
| 複雑度が上昇、カバレッジは横ばい | 新しい振る舞いが、既存関数の中の分岐追加として足された。 | レビュー前に抽出を要求する。エージェントのコード成長で最も多い形。 |
| カバレッジが跳ね、複雑度は不変 | テストが追加された。それが何かを主張しているかは、まだ何も確かめられていない。 | 数字を信じる前に、変更ファイルでミューテーションテストを走らせる。 |
| 1 つのメソッドがしきい値を大きく超える | モデルがプロンプトごとに、最初に見つけた場所へケースを足し続けた。 | 規則ごとに分割して再測定。新しいテストなしでスコアが崩れることが多い。 |
| 誰も頼んでいないコードの複雑度 | 投機的な分岐、防御的経路、要求のないオプション。 | 削除する。どの要求も名指ししない未テストの複雑さは、取り除くのが最も安い。 |
| スコアは良好、カバレッジはスナップショットテスト由来 | スイートはすべてを実行し、ほとんど何も検査していない。 | 指標はカバレッジ入力を通してあなたに嘘をついている。直すのはスコアではなくテスト。 |
実際に使う
式はコード 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;
} エージェントが言い抜けできないゲート
shコードを書いていない工程が、この順序で 2 つのチェックを走らせます。1 つ目はこのコードが変更できる形をしていると言い、2 つ目はそれを守るテストが変更時に本当に反応すると言います。
# 1. structural: is this changeable code?
crap-report --changed-only --threshold 30 || exit 1
# 2. behavioural: do the new tests assert anything?
stryker run --incremental --mutate "$(git diff --name-only origin/main...)"
# report both on the PR. one number is about the code,
# the other is about the tests that claim to cover it. 全員を苛立たせずに使う方法
2 つのレバーを別々に読む
- - 複雑度が押し上げた高スコアはリファクタリングの仕事であり、テストの仕事ではない
- - カバレッジ不足が押し上げた高スコアはテストの仕事であり、しかも安い
- - スコアの隣に必ず CC とカバレッジを出す。数値だけではどちらなのか分からない
変更頻度で重み付けする
- - 変更リスクが問題になるのは、変更が起きる場所だけ
- - 4 年間触られていないファイルのスコア 200 は、毎週編集されるファイルの 60 より緊急度が低い
- - スコア × コミット頻度で並べ、その上位から片付ける
除外を明示する
- - 生成コード、アダプタ、網羅的な switch は、実際のリスクを増やさずに複雑度を膨らませる
- - しきい値を黙って上げるのではなく、理由をコメントに書いて設定で除外する
- - 守っていたコードが生成されなくなったら、除外リストを見直す
よくある誤読
品質スコアとして扱う
CRAP が見積もるのはコードを変更するリスクです。コードが正しいか、命名が良いか、設計が良いかは何も語りません。本質的な業務複雑度を持つきれいでよくカバーされたメソッドも、不快なメソッドと同じスコアになります。
カバレッジで細工する
何でもスナップショットするテストや、アサーションのない通し実行は、数値を動かすだけでリスクを動かしません。CRAP をゲートにするなら、レビューかミューテーションテストか、その両方でテストを誠実に保つ何かが必要です。ループにエージェントがいるなら、別のチェックが防がない限りそれは起きると想定してください。
片付けの大型プロジェクトを立ち上げる
レガシーコードにリポジトリ全体の CRAP レポートを当てると、数値が大きすぎて無視されます。代わりに変更されたコードにラチェットをかけ、危険な部分はもともと触る予定だった人、あるいはエージェントに直させましょう。
保守されたツールを期待する
元の Crap4j は長年休眠しています。.NET では NDepend がこの指標を持ち、Rust、.NET、Groovy にはコミュニティ実装があり、Uncle Bob は自分のエージェント群のために crap4j、crap4go、crap4clj を保守しています。多くのスタックではすでに集めているデータから自分で計算します。
1 つの数、1 つの問い
CRAP は品質スコアではなく、そう意図されたこともありません。答えるのはもっと狭く、もっと有用な問いです。来週誰かがこのメソッドを編集したら、静かに何かが壊れる確率はどれくらいか。複雑度が「間違え方が何通りあるか」を、カバレッジが「そのうち何通りを誰かが見ているか」を語り、式は後者を前者より重く扱います。
だからこれは QA の照準器であり、AI 支援作業の監査道具になります。diff に向け、新規および変更されたコードをしきい値でゲートし、残りはスコアと変更頻度で並べ、2 つの入力を見えるままにして、数値が「このメソッドを分割する」か「テストする」という行動に解けるようにしてください。ただし、どちらの半分が著者に偽装できるかは忘れないこと。複雑度はコードが何であるかであり、カバレッジはテストが何を主張しているかにすぎません。その主張を検めるのがミューテーションテストです。
関連するエンジニアリング記事
この式のカバレッジ側こそがミューテーションテストの追及対象であり、どちらの指標も実際に動いているエージェント群では専任ロールを与えられています。
よくある質問
CRAP のスコアはどこから悪いのですか?
30 が慣例的な既定しきい値で、Crap4j から受け継がれ、後続の実装の多くがそのまま使っています。線を守る負担が小さい新規コードでは下げ、レガシーコードではラチェットを進めながら維持してください。レポートの見栄えのために上げるのは避けます。
AI 生成コードのレビューに CRAP は役立ちますか?
最も有用で安価なシグナルの 1 つです。複雑度はモデルが生み出したコードについての構造的事実であり、議論の余地がありません。リポジトリではなく diff を採点し、カバレッジが横ばいのまま複雑度が上がる形を警戒し、必ずミューテーション実行と組ませて、何も主張しないテストでカバレッジ側が膨らまされないようにしてください。
エージェントが書いたブランチにはどのしきい値を設定すべきですか?
人間より厳しく、しかもスコアではなく複雑度で表現します。実務的なパターン、そして Uncle Bob の群れが使っているパターンは、変更メソッドに複雑度 6 前後の硬い上限を課すことです。その水準ならカバレッジが何をしても CRAP スコアはほとんど上がれず、交渉の余地が残りません。
エージェントは自分の CRAP スコアを直せますか?
複雑度側は多くの場合直せますし、それは本物の改善なので自動化する価値があります。カバレッジ側は注意が必要です。カバレッジを上げる最も安い方法は、検査せずにコードを実行することだからです。ゲートはコードを書いていない工程で走らせ、新しいテストはミューテーションテストで検めてください。
なぜ複雑度は 2 乗で、未テスト率は 3 乗なのですか?
カバレッジのほうが複雑度より速くスコアを動かすためです。3 乗の項はカバレッジが完全に近づくとゼロへ崩れ、複雑なコードをテストする行為に即座の見返りを与えます。一方、2 乗の項と末尾の複雑度は、どれだけテストしても消えない床を残します。
CRAP でビルドを失敗させるべきですか?
新規および変更されたメソッドへのラチェットとしては、はい。強制でき、誰も予定を組む必要がありません。既存のコードベース全体に対するゲートとしては、いいえ。それは行動の変化ではなくバックログを生みます。