CRAP Score Analysis
When to Use This Skill
Use this skill when:
- Evaluating code quality and test coverage before changes
- Identifying high-risk code that needs refactoring or testing
- Setting up coverage collection for a .NET project
- Prioritizing which code to test based on risk
- Establishing coverage thresholds for CI/CD pipelines
What is CRAP?
CRAP Score = Complexity x (1 - Coverage)^2
The CRAP (Change Risk Anti-Patterns) score combines cyclomatic complexity with test coverage to identify risky code.
| CRAP Score |
Risk Level |
Action Required |
| < 5 |
Low |
Well-tested, maintainable code |
| 5-30 |
Medium |
Acceptable but watch complexity |
| > 30 |
High |
Needs tests or refactoring |
Why CRAP Matters
- High complexity + low coverage = danger: Code that's hard to understand AND untested is risky to modify
- Complexity alone isn't enough: A complex method with 100% coverage is safer than a simple method with 0%
- Focuses effort: Prioritize testing on complex code, not simple getters/setters
CRAP Score Examples
| Method |
Complexity |
Coverage |
Calculation |
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 Collection Setup
coverage.runsettings
Create a coverage.runsettings file in your repository root. The OpenCover format is required for CRAP score calculation because it includes cyclomatic complexity metrics.
<?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>
Key Configuration Options
| Option |
Purpose |
Format |
Must include opencover for complexity metrics |
Exclude |
Exclude test/benchmark assemblies by pattern |
ExcludeByAttribute |
Skip generated, obsolete, and explicitly excluded code (includes ExcludeFromCodeCoverageAttribute) |
ExcludeByFile |
Skip source-generated files, Blazor components, and migrations |
SkipAutoProps |
Don't count auto-properties as branches |
ReportGenerator Installation
Install ReportGenerator as a local tool for generating HTML reports with Risk Hotspots.
Add to .config/dotnet-tools.json
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "5.4.5",
"commands": ["reportgenerator"],
"rollForward": false
}
}
}
Then restore:
dotnet tool restore
Or Install Globally
dotnet tool install --global dotnet-reportgenerator-globaltool
Collecting Coverage
Run Tests with Coverage Collection
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
Generate HTML Report
dotnet reportgenerator \
-reports:"TestResults/**/coverage.opencover.xml" \
-targetdir:"coverage" \
-reporttypes:"Html;TextSummary;MarkdownSummaryGithub"
Report Types
| Type |
Description |
Output |
Html |
Full interactive report |
coverage/index.html |
TextSummary |
Plain text summary |
coverage/Summary.txt |
MarkdownSummaryGithub |
GitHub-compatible markdown |
coverage/SummaryGithub.md |
Badges |
SVG badges for README |
coverage/badge_*.svg |
Cobertura |
Merged Cobertura XML |
coverage/Cobertura.xml |
Reading the Report
Risk Hotspots Section
The HTML report includes a Risk Hotspots section showing methods sorted by complexity:
- Cyclomatic Complexity: Number of independent paths through code (if/else, switch cases, loops)
- NPath Complexity: Number of acyclic execution paths (exponential growth with nesting)
- Crap Score: Calculated from complexity and coverage
Interpreting Results
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
Action items:
ValidateToken() has CRAP > 30 with 0% coverage - test immediately or refactor
ParseRecord() is complex but has decent coverage - acceptable
CreateUser() and Calculate() are well-tested - safe to modify
Coverage Thresholds
Recommended Standards
| Coverage Type |
Target |
Action |
| Line Coverage |
> 80% |
Good for most projects |
| Branch Coverage |
> 60% |
Catches conditional logic |
| CRAP Score |
< 30 |
Maximum for new code |
Configuring Thresholds
Create coverage.props in your repository:
<Project>
<PropertyGroup>
<CoverageThresholdLine>80</CoverageThresholdLine>
<CoverageThresholdBranch>60</CoverageThresholdBranch>
</PropertyGroup>
</Project>
CI/CD Integration
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: