crap-analysis
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCRAP Score Analysis
CRAP分数分析
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
在以下场景使用该技能:
- 在代码变更前评估代码质量和测试覆盖率
- 识别需要重构或补充测试的高风险代码
- 为.NET项目设置覆盖率收集配置
- 根据风险优先级确定测试代码的顺序
- 为CI/CD流水线设置覆盖率阈值
What is CRAP?
什么是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 |
CRAP分数 = 圈复杂度 × (1 - 覆盖率)²
CRAP(变更风险反模式)分数结合圈复杂度和测试覆盖率来识别有风险的代码。
| CRAP分数 | 风险等级 | 所需操作 |
|---|---|---|
| < 5 | 低 | 测试充分、可维护的代码 |
| 5-30 | 中 | 可接受,但需关注复杂度 |
| > 30 | 高 | 需要补充测试或重构 |
Why CRAP Matters
CRAP的重要性
- 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
- 高复杂度+低覆盖率=危险: 难以理解且未测试的代码在修改时风险极高
- 仅看复杂度不够: 一个覆盖率100%的复杂方法比覆盖率0%的简单方法更安全
- 聚焦工作重点: 优先为复杂代码编写测试,而非简单的getter/setter方法
CRAP Score Examples
CRAP分数示例
| Method | Complexity | Coverage | Calculation | CRAP |
|---|---|---|---|---|
| 1 | 0% | 1 x (1 - 0)^2 | 1 |
| 54 | 52% | 54 x (1 - 0.52)^2 | 12.4 |
| 20 | 0% | 20 x (1 - 0)^2 | 20 |
| 45 | 20% | 45 x (1 - 0.20)^2 | 28.8 |
| 80 | 10% | 80 x (1 - 0.10)^2 | 64.8 |
| 方法 | 圈复杂度 | 覆盖率 | 计算过程 | CRAP分数 |
|---|---|---|---|---|
| 1 | 0% | 1 × (1 - 0)² | 1 |
| 54 | 52% | 54 × (1 - 0.52)² | 12.4 |
| 20 | 0% | 20 × (1 - 0)² | 20 |
| 45 | 20% | 45 × (1 - 0.20)² | 28.8 |
| 80 | 10% | 80 × (1 - 0.10)² | 64.8 |
Coverage Collection Setup
覆盖率收集配置
coverage.runsettings
coverage.runsettings
Create a file in your repository root. The OpenCover format is required for CRAP score calculation because it includes cyclomatic complexity metrics.
coverage.runsettingsxml
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat code coverage">
<Configuration>
<!-- OpenCover format includes cyclomatic complexity for CRAP scores -->
<Format>cobertura,opencover</Format>
<!-- Exclude test and benchmark assemblies -->
<Exclude>[*.Tests]*,[*.Benchmark]*,[*.Migrations]*</Exclude>
<!-- Exclude generated code, obsolete members, and explicit exclusions -->
<ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute</ExcludeByAttribute>
<!-- Exclude source-generated files, Blazor generated code, and migrations -->
<ExcludeByFile>**/obj/**/*,**/*.g.cs,**/*.designer.cs,**/*.razor.g.cs,**/*.razor.css.g.cs,**/Migrations/**/*</ExcludeByFile>
<!-- Exclude test projects -->
<IncludeTestAssembly>false</IncludeTestAssembly>
<!-- Optimization flags -->
<SingleHit>false</SingleHit>
<UseSourceLink>true</UseSourceLink>
<SkipAutoProps>true</SkipAutoProps>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>在仓库根目录创建文件。CRAP分数计算必须使用OpenCover格式,因为它包含圈复杂度指标。
coverage.runsettingsxml
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat code coverage">
<Configuration>
<!-- OpenCover format includes cyclomatic complexity for CRAP scores -->
<Format>cobertura,opencover</Format>
<!-- Exclude test and benchmark assemblies -->
<Exclude>[*.Tests]*,[*.Benchmark]*,[*.Migrations]*</Exclude>
<!-- Exclude generated code, obsolete members, and explicit exclusions -->
<ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute</ExcludeByAttribute>
<!-- Exclude source-generated files, Blazor generated code, and migrations -->
<ExcludeByFile>**/obj/**/*,**/*.g.cs,**/*.designer.cs,**/*.razor.g.cs,**/*.razor.css.g.cs,**/Migrations/**/*</ExcludeByFile>
<!-- Exclude test projects -->
<IncludeTestAssembly>false</IncludeTestAssembly>
<!-- Optimization flags -->
<SingleHit>false</SingleHit>
<UseSourceLink>true</UseSourceLink>
<SkipAutoProps>true</SkipAutoProps>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>Key Configuration Options
关键配置选项
| Option | Purpose |
|---|---|
| Must include |
| Exclude test/benchmark assemblies by pattern |
| Skip generated, obsolete, and explicitly excluded code (includes |
| Skip source-generated files, Blazor components, and migrations |
| Don't count auto-properties as branches |
| 选项 | 用途 |
|---|---|
| 必须包含 |
| 通过模式排除测试/基准测试程序集 |
| 跳过生成代码、过时成员和显式排除的代码(包括 |
| 跳过源生成文件、Blazor组件和迁移文件 |
| 不将自动属性计入分支 |
ReportGenerator Installation
ReportGenerator安装
Install ReportGenerator as a local tool for generating HTML reports with Risk Hotspots.
安装ReportGenerator作为本地工具,用于生成包含风险热点的HTML报告。
Add to .config/dotnet-tools.json
添加到.config/dotnet-tools.json
json
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "5.4.5",
"commands": ["reportgenerator"],
"rollForward": false
}
}
}Then restore:
bash
dotnet tool restorejson
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "5.4.5",
"commands": ["reportgenerator"],
"rollForward": false
}
}
}然后执行恢复命令:
bash
dotnet tool restoreOr Install Globally
或全局安装
bash
dotnet tool install --global dotnet-reportgenerator-globaltoolbash
dotnet tool install --global dotnet-reportgenerator-globaltoolCollecting Coverage
收集覆盖率数据
Run Tests with Coverage Collection
运行测试并收集覆盖率
bash
undefinedbash
undefinedClean previous results
清理之前的结果
rm -rf coverage/ TestResults/
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
--settings coverage.runsettings
--collect:"XPlat Code Coverage"
--results-directory ./TestResults
dotnet test tests/MyApp.Tests.Unit
--settings coverage.runsettings
--collect:"XPlat Code Coverage"
--results-directory ./TestResults
--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
--settings coverage.runsettings
--collect:"XPlat Code Coverage"
--results-directory ./TestResults
undefineddotnet test tests/MyApp.Tests.Integration
--settings coverage.runsettings
--collect:"XPlat Code Coverage"
--results-directory ./TestResults
--settings coverage.runsettings
--collect:"XPlat Code Coverage"
--results-directory ./TestResults
undefinedGenerate HTML Report
生成HTML报告
bash
dotnet reportgenerator \
-reports:"TestResults/**/coverage.opencover.xml" \
-targetdir:"coverage" \
-reporttypes:"Html;TextSummary;MarkdownSummaryGithub"bash
dotnet reportgenerator \
-reports:"TestResults/**/coverage.opencover.xml" \
-targetdir:"coverage" \
-reporttypes:"Html;TextSummary;MarkdownSummaryGithub"Report Types
报告类型
| Type | Description | Output |
|---|---|---|
| Full interactive report | |
| Plain text summary | |
| GitHub-compatible markdown | |
| SVG badges for README | |
| Merged Cobertura XML | |
| 类型 | 描述 | 输出位置 |
|---|---|---|
| 完整交互式报告 | |
| 纯文本摘要 | |
| 兼容GitHub的Markdown格式 | |
| 用于README的SVG徽章 | |
| 合并后的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
HTML报告包含风险热点部分,按复杂度排序展示方法:
- Cyclomatic Complexity: 代码中的独立路径数量(if/else、switch分支、循环等)
- NPath Complexity: 无环执行路径的数量(嵌套会导致指数级增长)
- Crap Score: 由复杂度和覆盖率计算得出的分数
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.0Action items:
- has CRAP > 30 with 0% coverage - test immediately or refactor
ValidateToken() - is complex but has decent coverage - acceptable
ParseRecord() - and
CreateUser()are well-tested - safe to modifyCalculate()
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行动项:
- 的CRAP分数>30且覆盖率为0% - 立即补充测试或重构
ValidateToken() - 复杂度较高但覆盖率尚可 - 可接受
ParseRecord() - 和
CreateUser()测试充分 - 修改时风险低Calculate()
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 |
| 覆盖率类型 | 目标值 | 行动 |
|---|---|---|
| 行覆盖率 | > 80% | 适用于大多数项目 |
| 分支覆盖率 | > 60% | 覆盖条件逻辑测试 |
| CRAP分数 | < 30 | 新代码的最高允许值 |
Configuring Thresholds
配置阈值
Create in your repository:
coverage.propsxml
<Project>
<PropertyGroup>
<!-- Coverage thresholds for CI enforcement -->
<CoverageThresholdLine>80</CoverageThresholdLine>
<CoverageThresholdBranch>60</CoverageThresholdBranch>
</PropertyGroup>
</Project>在仓库中创建文件:
coverage.propsxml
<Project>
<PropertyGroup>
<!-- Coverage thresholds for CI enforcement -->
<CoverageThresholdLine>80</CoverageThresholdLine>
<CoverageThresholdBranch>60</CoverageThresholdBranch>
</PropertyGroup>
</Project>CI/CD Integration
CI/CD集成
GitHub Actions
GitHub Actions
yaml
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.mdyaml
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.mdAzure Pipelines
Azure Pipelines
yaml
- 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'yaml
- 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'Quick Reference
快速参考
One-Liner Commands
一键命令
bash
undefinedbash
undefinedFull 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"
dotnet test --settings coverage.runsettings
--collect:"XPlat Code Coverage"
--results-directory ./TestResults &&
dotnet reportgenerator
-reports:"TestResults/**/coverage.opencover.xml"
-targetdir:"coverage"
-reporttypes:"Html;TextSummary"
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"
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
cat coverage/Summary.txt
Open HTML report (Linux)
打开HTML报告(Linux)
xdg-open coverage/index.html
xdg-open coverage/index.html
Open HTML report (macOS)
打开HTML报告(macOS)
open coverage/index.html
open coverage/index.html
Open HTML report (Windows)
打开HTML报告(Windows)
start coverage/index.html
undefinedstart coverage/index.html
undefinedProject Standards
项目标准
| Metric | New Code | Legacy Code |
|---|---|---|
| Line Coverage | 80%+ | 60%+ (improve gradually) |
| Branch Coverage | 60%+ | 40%+ (improve gradually) |
| Maximum CRAP | 30 | Document exceptions |
| High-risk methods | Must have tests | Add tests before modifying |
| 指标 | 新代码 | 遗留代码 |
|---|---|---|
| 行覆盖率 | 80%+ | 60%+(逐步提升) |
| 分支覆盖率 | 60%+ | 40%+(逐步提升) |
| 最高CRAP分数 | 30 | 记录例外情况 |
| 高风险方法 | 必须有测试 | 修改前先补充测试 |
What Gets Excluded
排除内容说明
The recommended excludes:
coverage.runsettings| Pattern | Reason |
|---|---|
| Test assemblies aren't production code |
| Benchmark projects |
| Database migrations (generated) |
| Source generators |
| Compiler-generated code |
| Explicit developer opt-out |
| Generated files |
| Blazor component generated code |
| Blazor CSS isolation generated code |
| EF Core migrations (auto-generated) |
| Auto-properties (trivial branches) |
推荐的会排除以下内容:
coverage.runsettings| 模式 | 原因 |
|---|---|
| 测试程序集不属于生产代码 |
| 基准测试项目 |
| 数据库迁移文件(自动生成) |
| 源生成器生成的代码 |
| 编译器生成的代码 |
| 开发者显式标记排除的代码 |
| 自动生成的文件 |
| Blazor组件生成的代码 |
| Blazor CSS隔离生成的代码 |
| EF Core迁移文件(自动生成) |
| 自动属性(无意义的分支) |
When to Update Thresholds
何时更新阈值
Lower thresholds temporarily for:
- Legacy codebases being modernized (document in README)
- Generated code that can't be modified
- Third-party wrapper code
Never lower thresholds for:
- "It's too hard to test" - refactor instead
- "We'll add tests later" - add them now
- New features - should meet standards from the start
可临时降低阈值的场景:
- 正在现代化改造的遗留代码库(需在README中说明)
- 无法修改的生成代码
- 第三方包装器代码
绝对不能降低阈值的场景:
- "测试难度太大" - 应优先重构
- "以后再补测试" - 现在就添加
- 新功能 - 从一开始就需符合标准
Additional Resources
额外资源
- Coverlet Documentation: https://github.com/coverlet-coverage/coverlet
- ReportGenerator: https://github.com/danielpalme/ReportGenerator
- CRAP Score Original Paper: http://www.artima.com/weblogs/viewpost.jsp?thread=215899
- Coverlet文档: https://github.com/coverlet-coverage/coverlet
- ReportGenerator: https://github.com/danielpalme/ReportGenerator
- CRAP分数原始论文: http://www.artima.com/weblogs/viewpost.jsp?thread=215899