CRAP代码风险分析Skill crap-analysis

CRAP代码风险分析技能用于评估软件代码质量,通过计算CRAP(变更风险反模式)分数,结合圈复杂度和测试覆盖率识别高风险代码。该技能提供完整的.NET项目覆盖率收集方案,使用OpenCover格式和ReportGenerator工具生成风险热点报告,帮助开发团队优先测试复杂代码、设置CI/CD覆盖率阈值,并指导代码重构决策。关键词:代码覆盖率、CRAP分数、圈复杂度、风险热点、测试策略、代码质量、.NET测试、覆盖率报告、重构指导、DevOps质量门禁。

测试 0 次安装 0 次浏览 更新于 2/26/2026

name: crap-analysis description: 分析代码覆盖率和CRAP(变更风险反模式)分数,识别高风险代码。使用OpenCover格式配合ReportGenerator生成风险热点报告,展示圈复杂度和未测试代码路径。 invocable: true

CRAP分数分析

何时使用此技能

在以下情况下使用此技能:

  • 变更前评估代码质量和测试覆盖率
  • 识别需要重构或测试的高风险代码
  • 为.NET项目设置覆盖率收集
  • 基于风险确定测试优先级
  • 为CI/CD流水线建立覆盖率阈值

什么是CRAP?

CRAP分数 = 复杂度 × (1 - 覆盖率)^2

CRAP(变更风险反模式)分数结合了圈复杂度和测试覆盖率来识别风险代码。

CRAP分数 风险等级 所需行动
< 5 测试充分,可维护的代码
5-30 可接受但需关注复杂度
> 30 需要测试或重构

CRAP的重要性

  • 高复杂度 + 低覆盖率 = 危险:难以理解且未经测试的代码修改风险高
  • 仅复杂度不够:100%覆盖率的复杂方法比0%覆盖率的简单方法更安全
  • 聚焦努力:优先测试复杂代码,而非简单的getter/setter

CRAP分数示例

方法 复杂度 覆盖率 计算 CRAP
GetUserId() 1 0% 1 × (1 - 0)^2 1
ParseToken() 54 52% 54 × (1 - 0.52)^2 12.4
ValidateForm() 20 0% 20 × (1 - 0)^2 20
ProcessOrder() 45 20% 45 × (1 - 0.20)^2 28.8
ImportData() 80 10% 80 × (1 - 0.10)^2 64.8

覆盖率收集设置

coverage.runsettings

在仓库根目录创建coverage.runsettings文件。必须使用OpenCover格式进行CRAP分数计算,因为它包含圈复杂度指标。

<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
  <DataCollectionRunSettings>
    <DataCollectors>
      <DataCollector friendlyName="XPlat code coverage">
        <Configuration>
          <!-- OpenCover格式包含用于CRAP分数的圈复杂度 -->
          <Format>cobertura,opencover</Format>

          <!-- 排除测试和基准测试程序集 -->
          <Exclude>[*.Tests]*,[*.Benchmark]*,[*.Migrations]*</Exclude>

          <!-- 排除生成代码、过时成员和显式排除项 -->
          <ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute</ExcludeByAttribute>

          <!-- 排除源生成文件、Blazor生成代码和迁移 -->
          <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

收集覆盖率

运行测试并收集覆盖率

# 清理先前结果
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

生成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 case、循环)
  • NPath复杂度:非循环执行路径数(嵌套时呈指数增长)
  • Crap分数:根据复杂度和覆盖率计算

解释结果

风险热点
─────────────
方法                          复杂度  覆盖率  Crap分数
──────────────────────────────────────────────────────────────────
DataImporter.ParseRecord()      54     52%    12.4
AuthService.ValidateToken()     32     0%     32.0   ← 高风险
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>
    <!-- CI执行的覆盖率阈值 -->
    <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'

快速参考

单行命令

# 完整分析工作流
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"

# 查看摘要
cat coverage/Summary.txt

# 打开HTML报告(Linux)
xdg-open coverage/index.html

# 打开HTML报告(macOS)
open coverage/index.html

# 打开HTML报告(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中记录)
  • 无法修改的生成代码
  • 第三方包装代码

绝不降低阈值的情况:

  • “测试太难” - 改为重构
  • “我们稍后添加测试” - 现在就添加
  • 新功能 - 从一开始就应满足标准

额外资源