crap-analysis

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

CRAP 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 ScoreRisk LevelAction Required
< 5LowWell-tested, maintainable code
5-30MediumAcceptable but watch complexity
> 30HighNeeds 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分数示例

MethodComplexityCoverageCalculationCRAP
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

方法圈复杂度覆盖率计算过程CRAP分数
GetUserId()
10%1 × (1 - 0)²1
ParseToken()
5452%54 × (1 - 0.52)²12.4
ValidateForm()
200%20 × (1 - 0)²20
ProcessOrder()
4520%45 × (1 - 0.20)²28.8
ImportData()
8010%80 × (1 - 0.10)²64.8

Coverage Collection Setup

覆盖率收集配置

coverage.runsettings

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
<?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>
在仓库根目录创建
coverage.runsettings
文件。CRAP分数计算必须使用OpenCover格式,因为它包含圈复杂度指标。
xml
<?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

关键配置选项

OptionPurpose
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

选项用途
Format
必须包含
opencover
以获取复杂度指标
Exclude
通过模式排除测试/基准测试程序集
ExcludeByAttribute
跳过生成代码、过时成员和显式排除的代码(包括
ExcludeFromCodeCoverageAttribute
ExcludeByFile
跳过源生成文件、Blazor组件和迁移文件
SkipAutoProps
不将自动属性计入分支

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 restore
json
{
  "version": 1,
  "isRoot": true,
  "tools": {
    "dotnet-reportgenerator-globaltool": {
      "version": "5.4.5",
      "commands": ["reportgenerator"],
      "rollForward": false
    }
  }
}
然后执行恢复命令:
bash
dotnet tool restore

Or Install Globally

或全局安装

bash
dotnet tool install --global dotnet-reportgenerator-globaltool

bash
dotnet tool install --global dotnet-reportgenerator-globaltool

Collecting Coverage

收集覆盖率数据

Run Tests with Coverage Collection

运行测试并收集覆盖率

bash
undefined
bash
undefined

Clean 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
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
undefined
dotnet test tests/MyApp.Tests.Integration
--settings coverage.runsettings
--collect:"XPlat Code Coverage"
--results-directory ./TestResults
undefined

Generate 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

报告类型

TypeDescriptionOutput
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

类型描述输出位置
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

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.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

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()
    测试充分 - 修改时风险低

Coverage Thresholds

覆盖率阈值

Recommended Standards

推荐标准

Coverage TypeTargetAction
Line Coverage> 80%Good for most projects
Branch Coverage> 60%Catches conditional logic
CRAP Score< 30Maximum for new code
覆盖率类型目标值行动
行覆盖率> 80%适用于大多数项目
分支覆盖率> 60%覆盖条件逻辑测试
CRAP分数< 30新代码的最高允许值

Configuring Thresholds

配置阈值

Create
coverage.props
in your repository:
xml
<Project>
  <PropertyGroup>
    <!-- Coverage thresholds for CI enforcement -->
    <CoverageThresholdLine>80</CoverageThresholdLine>
    <CoverageThresholdBranch>60</CoverageThresholdBranch>
  </PropertyGroup>
</Project>

在仓库中创建
coverage.props
文件:
xml
<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.md
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.md

Azure 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
undefined
bash
undefined

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"
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
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
undefined
start coverage/index.html
undefined

Project Standards

项目标准

MetricNew CodeLegacy Code
Line Coverage80%+60%+ (improve gradually)
Branch Coverage60%+40%+ (improve gradually)
Maximum CRAP30Document exceptions
High-risk methodsMust have testsAdd tests before modifying

指标新代码遗留代码
行覆盖率80%+60%+(逐步提升)
分支覆盖率60%+40%+(逐步提升)
最高CRAP分数30记录例外情况
高风险方法必须有测试修改前先补充测试

What Gets Excluded

排除内容说明

The recommended
coverage.runsettings
excludes:
PatternReason
[*.Tests]*
Test assemblies aren't production code
[*.Benchmark]*
Benchmark projects
[*.Migrations]*
Database migrations (generated)
GeneratedCodeAttribute
Source generators
CompilerGeneratedAttribute
Compiler-generated code
ExcludeFromCodeCoverageAttribute
Explicit developer opt-out
*.g.cs
,
*.designer.cs
Generated files
*.razor.g.cs
Blazor component generated code
*.razor.css.g.cs
Blazor CSS isolation generated code
**/Migrations/**/*
EF Core migrations (auto-generated)
SkipAutoProps
Auto-properties (trivial branches)

推荐的
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
自动属性(无意义的分支)

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

额外资源