关于 crap-analysis
面向 .NET 的 CRAP 分数分析技能,将复杂度与测试覆盖率结合,用于定位高风险方法并指导测试或重构。
主要功能
- 01计算 CRAP 风险分数
- 02收集兼容 OpenCover 的覆盖率
- 03生成 ReportGenerator 报告
- 04查看复杂度与覆盖率热点
- 05设置覆盖率和 CRAP 阈值
适用场景
- 01在修改代码前安排测试优先级
- 02识别复杂且覆盖不足的方法
- 03在 CI/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% 的简单方法更安全
- 帮助集中精力:优先测试复杂代码,而不是简单的 getter/setter
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
将 ReportGenerator 安装为本地工具,以生成包含风险热点的 HTML 报告。
添加到 .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 |
阅读报告
风险热点部分
HTML 报告包含一个 风险热点 部分,按复杂度列出方法:
- 圈复杂度:代码中独立路径的数量(if/else、switch 分支、循环)
- NPath 复杂度:无环执行路径的数量(嵌套会导致指数级增长)
- Crap 分数:根据复杂度和覆盖率计算
解读结果
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