crap-analysis について
複雑度とテストカバレッジを組み合わせた CRAP スコアで高リスクなメソッドを特定し、テストやリファクタリングを支援する .NET スキルです。
主な機能
- 01CRAP リスクスコアの計算
- 02OpenCover 対応カバレッジの収集
- 03ReportGenerator レポートの生成
- 04複雑度とカバレッジのホットスポット確認
- 05カバレッジと CRAP のしきい値設定
活用シーン
- 01コード変更前のテスト優先度付け
- 02複雑でカバレッジが低いメソッドの特定
- 03CI/CD へのカバレッジ検査の追加
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ aaronontheweb/ dotnet-skills --skill crap-analysisインストール後、Claude Code で/crap-analysisを入力して読み込まれたか確認します。
詳細
CRAP スコア分析
このスキルを使うタイミング
次の場合にこのスキルを使います:
- 変更前にコード品質とテストカバレッジを評価する
- リファクタリングやテストが必要な高リスクコードを特定する
- .NET プロジェクトでカバレッジ収集を設定する
- リスクに基づいてテスト対象のコードに優先順位を付ける
- CI/CD パイプラインのカバレッジしきい値を設定する
CRAP とは?
CRAP スコア = 複雑度 x (1 - カバレッジ)^2
CRAP(変更リスクのアンチパターン)スコアは、循環的複雑度とテストカバレッジを組み合わせて、リスクの高いコードを特定します。
| 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>
<Format>cobertura,opencover</Format>
<Exclude>[*.Tests]*,[*.Benchmark]*,[*.Migrations]*</Exclude>
<ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute</ExcludeByAttribute>
<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 のインストール
Risk Hotspots を含む 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
カバレッジの収集
カバレッジを収集しながらテストを実行する
# Clean previous results
rm -rf coverage/ TestResults/
# Run unit tests with coverage
dotnet test tests/MyApp.Tests.Unit \
--settings coverage.runsettings \
--collect:"XPlat Code Coverage" \
--results-directory ./TestResults
# Run integration tests (optional, adds to coverage)
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 互換の Markdown | coverage/SummaryGithub.md |
Badges | README 用の SVG バッジ | coverage/badge_*.svg |
Cobertura | 統合された Cobertura XML | coverage/Cobertura.xml |
レポートの読み方
Risk Hotspots セクション
HTML レポートには、複雑度順にメソッドを表示する Risk Hotspots セクションがあります:
- 循環的複雑度:コード内の独立した経路の数(if/else、switch のケース、ループ)
- NPath 複雑度:非循環実行経路の数(ネストによって指数関数的に増加します)
- Crap Score:複雑度とカバレッジに基づいて計算されます
結果の解釈
Risk Hotspots
─────────────
Method Complexity Coverage Crap Score
──────────────────────────────────────────────────────────────────
DataImporter.ParseRecord() 54 52% 12.4
AuthService.ValidateToken() 32 0% 32.0 ← HIGH RISK
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>
<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: 'Run tests with coverage'
inputs:
command: 'test'
arguments: '--settings coverage.runsettings --collect:"XPlat Code Coverage" --results-directory $(Build.SourcesDirectory)/TestResults'
- task: DotNetCoreCLI@2
displayName: 'Generate coverage report'
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: 'Publish coverage'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Build.SourcesDirectory)/coverage/Cobertura.xml'
クイックリファレンス
ワンライナーコマンド
# Full analysis workflow
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"
# View summary
cat coverage/Summary.txt
# Open HTML report (Linux)
xdg-open coverage/index.html
# Open HTML report (macOS)
open coverage/index.html
# Open HTML report (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
コメント
Markdown 対応