CRAPスコア分析
このスキルを使用するタイミング
次のような場面で使用します:
- 変更前のコード品質とテストカバレッジを評価する場合
- リファクタリングやテストが必要な高リスクコードを特定する場合
- .NETプロジェクトのカバレッジ収集を設定する場合
- リスクに基づいてテストすべきコードの優先順位を決める場合
- CI/CDパイプラインのカバレッジしきい値を設定する場合
CRAPとは?
CRAPスコア = 複雑度 x (1 - カバレッジ)^2
CRAP(Change Risk Anti-Patterns)スコアは、循環的複雑度とテストカバレッジを組み合わせて、リスクの高いコードを特定します。
| CRAPスコア | リスクレベル | 必要なアクション |
|---|---|---|
| < 5 | 低 | 十分にテストされ、保守可能なコード |
| 5-30 | 中 | 許容範囲だが複雑度に注意 |
| > 30 | 高 | テストまたはリファクタリングが必要 |
CRAPが重要な理由
- 高複雑度 + 低カバレッジ = 危険:理解が難しく、かつテストされていないコードは変更リスクが高い
- 複雑度だけでは不十分:複雑なメソッドでも100%カバレッジがあれば、単純なメソッドで0%カバレッジより安全
- 努力を集中:単純なゲッター/セッターではなく、複雑なコードにテストを優先する
CRAPスコアの例
| メソッド | 複雑度 | カバレッジ | 計算式 | CRAP |
|---|---|---|---|---|
GetUserId() | 1 | 0% | 1 x (1 - 0)^2 | 1 |
ParseToken() | 54 | 52% | 54 x (1 - 0.52)^2 | 12.4 |
ValidateForm() | 20 | 0% | 20 x (1 - 0)^2 | 20 |
ProcessOrder() | 45 | 20% | 45 x (1 - 0.20)^2 | 28.8 |
ImportData() | 80 | 10% | 80 x (1 - 0.10)^2 | 64.8 |
カバレッジ収集のセットアップ
coverage.runsettings
リポジトリルートに coverage.runsettings ファイルを作成します。CRAPスコアの計算にはOpenCover形式が必須です。この形式には循環的複雑度のメトリクスが含まれているためです。
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat code coverage">
<Configuration>
<!-- OpenCover形式にはCRAPスコア用の循環的複雑度が含まれます -->
<Format>cobertura,opencover</Format>
<!-- テストとベンチマークアセンブリを除外 -->
<Exclude>[*.Tests]*,[*.Benchmark]*,[*.Migrations]*</Exclude>
<!-- 生成コード、廃止メンバー、明示的除外を除外 -->
<ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute</ExcludeByAttribute>
<!-- ソース生成ファイル、Blazor生成コード、マイグレーションを除外 -->
<ExcludeByFile>**/obj/**/*,**/*.g.cs,**/*.designer.cs,**/*.razor.g.cs,**/*.razor.css.g.cs,**/Migrations/**/*</ExcludeByFile>
<!-- テストプロジェクトを除外 -->
<IncludeTestAssembly>false</IncludeTestAssembly>
<!-- 最適化フラグ -->
<SingleHit>false</SingleHit>
<UseSourceLink>true</UseSourceLink>
<SkipAutoProps>true</SkipAutoProps>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>
主要な設定オプション
| オプション | 目的 |
|---|---|
Format | 複雑度メトリクスのために opencover を含める必要あり |
Exclude | テスト/ベンチマークアセンブリをパターンで除外 |
ExcludeByAttribute | 生成コード、廃止コード、明示的に除外されたコード(ExcludeFromCodeCoverageAttribute を含む)をスキップ |
ExcludeByFile | ソース生成ファイル、Blazorコンポーネント、マイグレーションをスキップ |
SkipAutoProps | 自動プロパティをブランチとしてカウントしない |
ReportGeneratorのインストール
リスクホットスポットを含むHTMLレポートを生成するために、ReportGeneratorをローカルツールとしてインストールします。
.config/dotnet-tools.json に追加
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "5.4.5",
"commands": ["reportgenerator"],
"rollForward": false
}
}
}
その後復元:
dotnet tool restore
またはグローバルにインストール
dotnet tool install --global dotnet-reportgenerator-globaltool
カバレッジの収集
カバレッジ付きでテストを実行
# 以前の結果をクリーンアップ
rm -rf coverage/ TestResults/
# 単体テストをカバレッジ付きで実行
dotnet test tests/MyApp.Tests.Unit \
--settings coverage.runsettings \
--collect:"XPlat Code Coverage" \
--results-directory ./TestResults
# 統合テストを実行(オプション、カバレッジに追加)
dotnet test tests/MyApp.Tests.Integration \
--settings coverage.runsettings \
--collect:"XPlat Code Coverage" \
--results-directory ./TestResults
HTMLレポートを生成
dotnet reportgenerator \
-reports:"TestResults/**/coverage.opencover.xml" \
-targetdir:"coverage" \
-reporttypes:"Html;TextSummary;MarkdownSummaryGithub"
レポートタイプ
| タイプ | 説明 | 出力 |
|---|---|---|
Html | 完全なインタラクティブレポート | coverage/index.html |
TextSummary | プレーンテキストサマリー | coverage/Summary.txt |
MarkdownSummaryGithub | GitHub互換マークダウン | coverage/SummaryGithub.md |
Badges | README用SVGバッジ | coverage/badge_*.svg |
Cobertura | マージされたCobertura XML | coverage/Cobertura.xml |
レポートの読み方
リスクホットスポットセクション
HTMLレポートには、複雑度でソートされたメソッドを示すリスクホットスポットセクションが含まれます:
- 循環的複雑度:コード内の独立したパスの数(if/else、switchケース、ループ)
- NPath複雑度:非巡回実行パスの数(ネストによる指数関数的増加)
- CRAPスコア:複雑度とカバレッジから計算
結果の解釈
リスクホットスポット
─────────────
メソッド 複雑度 カバレッジ CRAPスコア
──────────────────────────────────────────────────────────────────
DataImporter.ParseRecord() 54 52% 12.4
AuthService.ValidateToken() 32 0% 32.0 ← 高リスク
OrderProcessor.Calculate() 28 85% 1.3
UserService.CreateUser() 15 100% 0.0
アクション項目:
ValidateToken()はCRAP > 30、カバレッジ0% - すぐにテストするかリファクタリングParseRecord()は複雑だがカバレッジは十分 - 許容範囲CreateUser()とCalculate()は十分にテストされている - 変更しても安全
カバレッジしきい値
推奨基準
| カバレッジタイプ | 目標 | アクション |
|---|---|---|
| 行カバレッジ | > 80% | ほとんどのプロジェクトに適切 |
| ブランチカバレッジ | > 60% | 条件付きロジックを捕捉 |
| CRAPスコア | < 30 | 新しいコードの最大値 |
しきい値の設定
リポジトリに coverage.props を作成:
<Project>
<PropertyGroup>
<!-- CI適用のためのカバレッジしきい値 -->
<CoverageThresholdLine>80</CoverageThresholdLine>
<CoverageThresholdBranch>60</CoverageThresholdBranch>
</PropertyGroup>
</Project>
CI/CD統合
GitHub Actions
name: Coverage
on:
pull_request:
branches: [main, dev]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Restore tools
run: dotnet tool restore
- name: Run tests with coverage
run: |
dotnet test \
--settings coverage.runsettings \
--collect:"XPlat Code Coverage" \
--results-directory ./TestResults
- name: Generate report
run: |
dotnet reportgenerator \
-reports:"TestResults/**/coverage.opencover.xml" \
-targetdir:"coverage" \
-reporttypes:"Html;MarkdownSummaryGithub;Cobertura"
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
- name: Add coverage to PR
uses: marocchino/sticky-pull-request-comment@v2
with:
path: coverage/SummaryGithub.md
Azure Pipelines
- task: DotNetCoreCLI@2
displayName: 'カバレッジ付きでテストを実行'
inputs:
command: 'test'
arguments: '--settings coverage.runsettings --collect:"XPlat Code Coverage" --results-directory $(Build.SourcesDirectory)/TestResults'
- task: DotNetCoreCLI@2
displayName: 'カバレッジレポートを生成'
inputs:
command: 'custom'
custom: 'reportgenerator'
arguments: '-reports:"$(Build.SourcesDirectory)/TestResults/**/coverage.opencover.xml" -targetdir:"$(Build.SourcesDirectory)/coverage" -reporttypes:"HtmlInline_AzurePipelines;Cobertura"'
- task: PublishCodeCoverageResults@2
displayName: 'カバレッジを公開'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Build.SourcesDirectory)/coverage/Cobertura.xml'
クイックリファレンス
ワンライナーコマンド
# 完全な分析ワークフロー
rm -rf coverage/ TestResults/ && \
dotnet test --settings coverage.runsettings \
--collect:"XPlat Code Coverage" \
--results-directory ./TestResults && \
dotnet reportgenerator \
-reports:"TestResults/**/coverage.opencover.xml" \
-targetdir:"coverage" \
-reporttypes:"Html;TextSummary"
# サマリーを表示
cat coverage/Summary.txt
# HTMLレポートを開く(Linux)
xdg-open coverage/index.html
# HTMLレポートを開く(macOS)
open coverage/index.html
# HTMLレポートを開く(Windows)
start coverage/index.html
プロジェクト基準
| メトリクス | 新しいコード | レガシーコード |
|---|---|---|
| 行カバレッジ | 80%以上 | 60%以上(段階的に改善) |
| ブランチカバレッジ | 60%以上 | 40%以上(段階的に改善) |
| CRAP最大値 | 30 | 例外を文書化 |
| 高リスクメソッド | テスト必須 | 変更前にテストを追加 |
除外されるもの
推奨される coverage.runsettings では以下を除外:
| パターン | 理由 |
|---|---|
[*.Tests]* | テストアセンブリはプロダクションコードではない |
[*.Benchmark]* | ベンチマークプロジェクト |
[*.Migrations]* | データベースマイグレーション(生成) |
GeneratedCodeAttribute | ソースジェネレーター |
CompilerGeneratedAttribute | コンパイラ生成コード |
ExcludeFromCodeCoverageAttribute | 開発者による明示的な除外 |
*.g.cs, *.designer.cs | 生成ファイル |
*.razor.g.cs | Blazorコンポーネント生成コード |
*.razor.css.g.cs | Blazor CSS分離生成コード |
**/Migrations/**/* | EF Coreマイグレーション(自動生成) |
SkipAutoProps | 自動プロパティ(自明なブランチ) |
しきい値を更新するタイミング
以下の場合、一時的にしきい値を下げる:
- モダナイゼーション中のレガシーコードベース(READMEに文書化)
- 変更できない生成コード
- サードパーティラッパーコード
以下の場合、決してしきい値を下げない:
- 「テストが難しすぎる」 - 代わりにリファクタリング
- 「後でテストを追加する」 - 今すぐ追加する
- 新機能 - 最初から基準を満たすべき
追加リソース
- Coverlet ドキュメント: https://github.com/coverlet-coverage/coverlet
- ReportGenerator: https://github.com/danielpalme/ReportGenerator
- CRAPスコア原著論文: http://www.artima.com/weblogs/viewpost.jsp?thread=215899