mcpField
C
crap-analysis

crap-analysis

github.com/aaronontheweb测试与质量1.2k567 次安装今天更新
代码仓库

关于 crap-analysis

面向 .NET 的 CRAP 分数分析技能,将复杂度与测试覆盖率结合,用于定位高风险方法并指导测试或重构。

主要功能

  1. 01计算 CRAP 风险分数
  2. 02收集兼容 OpenCover 的覆盖率
  3. 03生成 ReportGenerator 报告
  4. 04查看复杂度与覆盖率热点
  5. 05设置覆盖率和 CRAP 阈值

适用场景

  1. 01在修改代码前安排测试优先级
  2. 02识别复杂且覆盖不足的方法
  3. 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()10%1 x (1 - 0)^21
ParseToken()5452%54 x (1 - 0.52)^212.4
ValidateForm()200%20 x (1 - 0)^220
ProcessOrder()4520%45 x (1 - 0.20)^228.8
ImportData()8010%80 x (1 - 0.10)^264.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 的 Markdowncoverage/SummaryGithub.md
Badges用于 README 的 SVG 徽章coverage/badge_*.svg
Cobertura合并后的 Cobertura XMLcoverage/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.csBlazor 组件生成的代码
*.razor.css.g.csBlazor CSS 隔离生成的代码
**/Migrations/**/*EF Core 迁移(自动生成)
SkipAutoProps自动属性(简单分支)

何时更新阈值

以下情况可临时降低阈值:

  • 正在现代化改造的遗留代码库(在 README 中记录)
  • 无法修改的生成代码
  • 第三方封装代码

以下情况绝不可降低阈值:

  • “太难测试了”——改为重构
  • “以后再添加测试”——现在就添加
  • 新功能——从一开始就应符合标准

其他资源

评论

支持 Markdown