使用 Fine Code Coverage 取得程式码覆盖範围

这是 Visual Studio 里的一个延伸模组 (Extension),大约在四五年前在 Visual Stuidio 2019 时就已经发布的一个工具,而我在过去带新人教单元测试时都会介绍这个工具,透过这个工具取得测试的程式码覆盖範围。

因为我平常的开发工具是使用 JetBrains Rider,已经有内建 Code Coverage 的功能,我只有在做教学或写文件、找问题、重现别人问题情境的时候才会开启 VS2022,在三月底四月初时这个工具产生 Code Coverage 的功能都还正常,但是却在前几天因为在整理文件时久违地开启 Visual Studio 2022 并且要取得 Code Coverage 却出现了异常,在找寻问题原因以及尝试如何解决花了不少的时间,最后是顺利地找到原因并且排除了状况。于是就写了这篇文章来介绍工具并说明要怎么解决异常状况。

程式码覆盖範围

Code Coverage 程式码覆盖範围,大多数也被称为程式码覆盖率。是衡量测试执行时,实际执行到专案中多少程式码的指标。常见指标包括:

  • 行级覆盖率 (Line Coverage):测试执行时,被执行到的程式码行数 ÷ 总程式码行数
  • 方法覆盖率 (Method/Function Coverage):测试执行时,有被呼叫到的方法(或函式)数 ÷ 总方法数

虽然覆盖率指标有助于快速发现未被测试的程式区段,但它无法评估测试本身的品质与完整度;测试边界、异常流程、业务情境等仍需依需求与案例分析。

相关连结:

  • 代码覆盖率 | wikipedia
  • 使用程式码涵盖範围进行单元测试 | Microsoft Learn

在「使用程式代码涵盖範围来判断要测试多少程序代码 | Microsoft Learn」这篇文章里就有提到:

若要判断专案程式码中经由测试的部分比例,例如单元测试,您可以使用 Visual Studio 的程式码覆盖率功能。 若要有效防範程式错误,您的测试应该执行或「涵盖」大部分的程式码。

但是在 VIsual Studio 里只有 Enterprise 这个版本才有内建程式码覆盖範围的功能

虽然我们也可以在 CLI 里使用 dotnet-coverage 工具来取得程式码覆盖範围的数据并且使用 Coverlet 与 ReportGenerator 产生报表,

https://learn.microsoft.com/zh-tw/dotnet/core/testing/unit-testing-code-coverage?tabs=windows#code-coverage-tooling

但是没有整合在 Visual Studio 里 (Professional, Community)  对于开发人员 (有在写测测试的)  就会觉得不方便也不直觉。

 

关于 Code Coverage 数据

Code Converage 数据所呈现的是有哪些程式码没有被测试所覆盖,这是用于提醒开发人员,而不是要做为一种衡量或评分、评判、打分数或是 KPI 的数据指标。

过去我在团队内导入与推动单元测试时,就有上上面的主管提出一些意见。因为既然要导入就要看到成效,而要看到执行成效就会需要有数据来做为依据,所以主管就提出要以 Code Coverage 做为各个开发人员在「单元测试」这个项目的 KPI 评量数据。

我当时极力的反对,甚至于提出离职方式来反对使用 Code Coverage 做为单元测试导入的 KPI 的决策。

就如同前面所说的,Code Coverage 是在于呈现有多少程式码有被测试所覆盖,并揭示还有多少是没有被覆盖,让开发人员在之后每次的提交都能够让去提高 Code Coverage 的比例。

因为导入单元测试是很花时间的,而且很多人对于测试的最大误解就是要多花时间去写更多的程式码,写测试对很多开发者的主观认知就是增加工作量。如果这时候因为团队要导入测试而且是 KPI 项目,然后要以 Code Coverage 做为衡量的标準,那么「人性」的发挥在此时就会完全体现,为了要让 Code Coverage 数据可以达标,就会有很多的作弊的方式出现,最简单的就是每个方法、程式码都会被测试所覆盖而且在执行时都会通过,但是每个测试方法都没有 Assert,用这样的方式就可以提高 Code Coverage 的数字,但这样的作法就完全地歪曲了一开始导入单元测试的初衷与本意了,而且这样的作弊还要浪费时间去写那些无用的测试程式码,还不如就不要导入单元测试。

所以这边要再次强调,Code Coverage 不可以做为 KPI 项目衡量的依据,如果真有团对这么做,请千万一定要提出异议与反对。

覆盖率数据可协助找出未测试程式码,但无法保证测试品质或决定测试是否有效。建议:

  • 依需求分析、使用案例、边界条件设计测试。
  • 将覆盖率视为「提醒」而非「目标」。
  • 不建议将 Code Coverage 作为单一 KPI 指标,以免忽略测试逻辑与效能。

覆盖率只是辅助:不应直接用百分比决定是否「测试足够」。

 

Fine Code Coverage

以前开发 .NET Framework 时期还有个 AxoCover 工具可以取得 Code Coverage,但是这个工具就无法应用在 .NET Core 专案。曾经有一段时间我是透过 CLI 下指令去取得 Code Coverage 与产生报告档案,但过了一阵子后就有同事跟我说有个 Visual Studio Extension 可以取得 Code Coverage 并与 Editor 有做整合,可以在 Editor 里就能以颜色标注覆盖範围,这工具就是 Fine Code Coverage。

在 Visual Studio 安装 Fine Code Coverage

  • Fine Code Coverage - Visual Studio Marketplace
  • FortuneN/FineCodeCoverage: Visualize unit test code coverage easily for free in Visual Studio Community Edition (and other editions too)

Visual Studio 上方功能列「延伸模组」→「管理延伸模组」

搜寻并安装「Fine Code Coverage」,重启 Visual Studio 后生效。

重启 Visual Studio 后,可以在「工具」看到 FCC Clear UI 与 FCC Toggle Indicators

然后在「检视」→「其他视窗」→「Fine Code Coverage」将 Fine Code Coverage  显示出来

Fine Code Coverage 视窗

不过因为还没有执行测试所以还不会有任何的资料呈现,有些 Fine Code Coverage 设定可以做调整。

Fine Code Coverage 设定

「工具」→「选项」

Run (Common) > Enabled 设定

  • 功能:整体开关,用来控制「是否启用覆盖率收集」。
  • 说明:如果你把它设为 False,即使按下「Run coverage」也不会启动 Coverlet/OpenCover,也不会在编辑器显示任何行号标记或产生报表;设为 True,Fine Code Coverage 才会在你执行测试时自动拦截并收集覆盖率。

Run (Coverlet / OpenCover) > RunInParallel 设定

  • 功能:决定 Coverlet/OpenCover 的「启动时机」是同步还是平行。
  • 说明:
    • 设为 False(预设):Fine Code Coverage 会先等测试专案的所有测试跑完,再启动 Coverlet 或 OpenCover 去分析测试过程中的执行资料
    • 设为 True:Fine Code Coverage 不会等到测试全部结束,会在测试执行的同时,就平行启动覆盖率工具收集资料,通常可以缩短整体收集时间,但某些情境下(如工具注入延迟)可能会导致少数测试结果遗漏。
我建议是设定为 False

Editor Colouring Line Highlighting > ShowLineCoverageHighlighting 与 ShowLineCoveredHighlighting 设定

ShowLineCoveredHighlighting 控制「在原始码编辑器中,对已被测试覆盖的程式码行,是否以底色(预设绿色)高亮显示」

功能说明:

  • 当设为 True 时,所有「已覆盖」(covered)的程式码行会在编辑器里带有绿色底色,让你一眼就能看出哪些程式码被测试命中。
  • 设为 False 则不做底色高亮,但如果你同时开启了「Glyph Margin」的对应设定,仍会在行号旁显示绿色图示。

搭配使用

  • ShowLineCoverageHighlighting(最上层的开关)必须为 True,才会启用任何行高亮。
  • ShowLineCoveredHighlighting 控制「已覆盖行」的底色。
  • 其它同组的选项如 ShowLineUncoveredHighlighting、ShowLinePartiallyCoveredHighlighting 则分别控制「未覆盖」与「部分覆盖」行的红/黄底色。

Exclude / Include (Common) > Include TestAssembly  设定

IncludeTestAssembly 这个选项控制「是否将测试专案本身也纳入覆盖率收集範围」:

  • True
    把测试专案 (测试组件,一般都会是 *Tests.dll) 里的程式码行(测试方法、辅助函式等等)纳进覆盖率报表。
  • False(预设)
    只收集「被测专案」的覆盖率,不包含测试专案。这样你在报表里看到的都是实际要测试的生产程式码,而不会被测试程式本身所佔行数稀释。

何时要设成 True?

  • 诊断测试程式品质:若你想知道自己的测试程式是否有「死角」或未执行到的测试辅助程式码,就可把它打开。
  • 多层次专案结构:有时候一个解决方案里既有 API、也有测试专案,你想快速一口气看所有程式(包含测试)的覆盖率,可暂时开启。

大多数场景建议设成 False

  • 以「测试覆盖生产程式码」为主要目的时,让测试专案自己不出现在报表里,数据才不会失真也更具可读性。

 

以上是几个基本的设定说明。

接着开启专案、重新建置方案,然后执行所有单元测试(没有执行的那个是整合测试专案)

接着开启 Fine Code Coverage 视窗,还是空空的,这是因为要产生 Code Coverage 报告是需要一点时间,会在所有测试都执行完毕后才会去产生 Code Coverage 报告,可以观察输出视窗的「测试」与「FCC」输出来源

当所有测试都完成后,接着看 FCC 的输出内容,可以看到各个测试专案接续执行产生 Code Coverage 结果,最后看到「==== DONE =====」出现就表示已完成

接着开启 Fine Code Coverage 视窗就可以看到 Code Coverage 报告

展开 Sample.Service 并点选 Sample.Service.Implements.ShipperService

会自动开启 ShipperService.cs,并且在 Editor 里会以颜色标示程式码是否有被测试所覆盖

覆盖的颜色会有三种:

  • 绿色 -  已覆盖
  • 黄色 -  部分覆盖
  • 红色 -  未被覆盖

看看另外一个类别 ShipperRepository.cs

这边就可以看到 CreateAsync 方法的程式码里有出现不同颜色的覆盖

如果不想一直看到程式码被颜色所覆盖,可以点选上方功能列「工具」→「FCC Toggle Indicators」

就会关闭程式码的颜色覆盖,当然也可以随时点选「FCC Toggle Indicators」查看 Code Coverage 的颜色覆盖

以上就是 Fine Code Coverage 的基本功能介绍,如果还想要多了解其他功能,可以再阅读以下连结的文章:

  • Visual Studio Code Coverage with Fine Code Coverage Visual Studio 2022 Extension - CodeSloth
  • Mastering Code Coverage Analysis for .NET Projects without Breaking the Bank | NimblePros Blog
  • 通过 Coverlet + ReportGenerator + Fine Code Coverage 产生测试涵盖率报表 | 余小章 @ 大内殿堂 - 点部落

 

Fine Code Coverage 无法产生 Code Coverage 报告的解决方式

第一种,因为专案路径里有出现特殊符号而导致无法产生 Code Coverage  报告

错误讯息里描述着指定目录里的 Sample.ReposotoryTests.coverage.xml 及 Sample.ServiceTests.coverage.xml 及 Sample.WebApplicationTests.coverage.xml 档案

然后实际开启档案总管并且到指定目录里是有看到 Sample.ServiceTests.coverage.xml 档案

研判是因为我将方案放在名称有 []符号的目录里,而导致了这个问题

D:\[Practise]\web_integration_tests\sample_xunit

之后我将方案搬移到另外一般名称的目录,就没有出现错误而且 Code Coverage 报告也有正确地产出。

ReportGenerator 在解析「-reports:」参数时,使用的是 glob 语法,在 glob 语法里 [...] 会被当成「字元集合」来解读,就导致整个路径都无法正确对应到实际档案,才会一直报「File does not exist」。

 

第二种:因为 CET 而导致

这个状况在我的 Windows 11 环境里没有出现,但是在我工作的 Windows 10 环境里就出现了这样的状况

[2025/4/30 3:30:42.598 下午] : Initializing
[2025/4/30 3:30:42.629 下午] : Initialized
[2025/4/30 3:30:43.460 下午] : ================================== COVERAGE STARTING - 1 ==================================
[2025/4/30 3:30:43.558 下午] : Coverlet Run (Lesson_02.LibraryTests) - Arguments
"D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\build-output\Lesson_02.LibraryTests.dll"
--format "cobertura"
--include "[Lesson_02.Library]*"
--include "[Lesson_02.LibraryTests]*"
--exclude-by-file "**/Migrations/*"
--exclude-by-attribute GeneratedCode
--include-test-assembly
--target "dotnet"
--threshold-type line
--threshold-stat total
--threshold 0
--output "D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml"
--targetargs "test  ""D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\build-output\Lesson_02.LibraryTests.dll"" --nologo --blame  --results-directory ""D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output"" --diag ""D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output/diagnostics.log""  "
[2025/4/30 3:30:44.494 下午] : Coverlet Run (Lesson_02.LibraryTests) - Output
CLR: Assert failure(PID 48212 [0x0000bc54], Thread: 26576 [0x67d0]): !AreCetShadowStacksEnabled() || UseSpecialUserModeApc()
    File: D:\a\_work\1\s\src\coreclr\vm\threads.cpp, Line: 8329 Image:
C:\Users\01002894\AppData\Local\FineCodeCoverage\coverlet\6.0.4\coverlet.console.6.0.4\coverlet.exe
[2025/4/30 3:30:44.494 下午] : Completed coverage for (Lesson_02.LibraryTests) : 00:00:00.9458401
[2025/4/30 3:30:44.763 下午] : ReportGenerator Run [reporttype:Cobertura] Error
2025-04-30T15:30:44: Arguments
2025-04-30T15:30:44:  -targetdir:D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output
2025-04-30T15:30:44:  -reports:D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml
2025-04-30T15:30:44:  -reporttypes:Cobertura
2025-04-30T15:30:44: The report file 'D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml' is invalid. File does not exist (Full path: 'D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml').
2025-04-30T15:30:44: No report files specified.
ExitCode : 1
[2025/4/30 3:30:44.765 下午] : ================================== ERROR ==================================
System.Exception: 2025-04-30T15:30:44: Arguments
2025-04-30T15:30:44:  -targetdir:D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output
2025-04-30T15:30:44:  -reports:D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml
2025-04-30T15:30:44:  -reporttypes:Cobertura
2025-04-30T15:30:44: The report file 'D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml' is invalid. File does not exist (Full path: 'D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml').
2025-04-30T15:30:44: No report files specified.
   于 FineCodeCoverage.Engine.ReportGenerator.ReportGeneratorUtil.<>c__DisplayClass47_0.<<GenerateAsync>g__RunAsync|0>d.MoveNext()
--- 先前掷回例外状况之位置中的堆叠追蹤结尾 ---
   于 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   于 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   于 FineCodeCoverage.Engine.ReportGenerator.ReportGeneratorUtil.<GenerateAsync>d__47.MoveNext()
--- 先前掷回例外状况之位置中的堆叠追蹤结尾 ---
   于 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   于 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   于 FineCodeCoverage.Engine.FCCEngine.<RunAndProcessReportAsync>d__38.MoveNext()
--- 先前掷回例外状况之位置中的堆叠追蹤结尾 ---
   于 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   于 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   于 FineCodeCoverage.Engine.FCCEngine.<>c__DisplayClass44_0.<<ReloadCoverage>b__0>d.MoveNext()
--- 先前掷回例外状况之位置中的堆叠追蹤结尾 ---
   于 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   于 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   于 FineCodeCoverage.Engine.FCCEngine.<>c__DisplayClass43_0.<<RunCancellableCoverageTask>b__1>d.MoveNext()
[2025/4/30 3:30:55.158 下午] : ================================== COVERAGE STARTING - 2 ==================================
[2025/4/30 3:30:55.384 下午] : Coverlet Run (Lesson_02.LibraryTests) - Arguments
"D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\build-output\Lesson_02.LibraryTests.dll"
--format "cobertura"
--include "[Lesson_02.Library]*"
--include "[Lesson_02.LibraryTests]*"
--exclude-by-file "**/Migrations/*"
--exclude-by-attribute GeneratedCode
--include-test-assembly
--target "dotnet"
--threshold-type line
--threshold-stat total
--threshold 0
--output "D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml"
--targetargs "test  ""D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\build-output\Lesson_02.LibraryTests.dll"" --nologo --blame  --results-directory ""D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output"" --diag ""D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output/diagnostics.log""  "
[2025/4/30 3:30:56.213 下午] : Coverlet Run (Lesson_02.LibraryTests) - Output
CLR: Assert failure(PID 12012 [0x00002eec], Thread: 47668 [0xba34]): !AreCetShadowStacksEnabled() || UseSpecialUserModeApc()
    File: D:\a\_work\1\s\src\coreclr\vm\threads.cpp, Line: 8329 Image:
C:\Users\01002894\AppData\Local\FineCodeCoverage\coverlet\6.0.4\coverlet.console.6.0.4\coverlet.exe
[2025/4/30 3:30:56.224 下午] : Completed coverage for (Lesson_02.LibraryTests) : 00:00:00.8700435
[2025/4/30 3:30:56.477 下午] : ReportGenerator Run [reporttype:Cobertura] Error
2025-04-30T15:30:56: Arguments
2025-04-30T15:30:56:  -targetdir:D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output
2025-04-30T15:30:56:  -reports:D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml
2025-04-30T15:30:56:  -reporttypes:Cobertura
2025-04-30T15:30:56: The report file 'D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml' is invalid. File does not exist (Full path: 'D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml').
2025-04-30T15:30:56: No report files specified.
ExitCode : 1
[2025/4/30 3:30:56.478 下午] : ================================== ERROR ==================================
System.Exception: 2025-04-30T15:30:56: Arguments
2025-04-30T15:30:56:  -targetdir:D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output
2025-04-30T15:30:56:  -reports:D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml
2025-04-30T15:30:56:  -reporttypes:Cobertura
2025-04-30T15:30:56: The report file 'D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml' is invalid. File does not exist (Full path: 'D:\Projects\教育训练_进阶\单元测试\Lesson_02\Lesson_02.LibraryTests\bin\Debug\net8.0\fine-code-coverage\coverage-tool-output\Lesson_02.LibraryTests.coverage.xml').
2025-04-30T15:30:56: No report files specified.
   于 FineCodeCoverage.Engine.ReportGenerator.ReportGeneratorUtil.<>c__DisplayClass47_0.<<GenerateAsync>g__RunAsync|0>d.MoveNext()
--- 先前掷回例外状况之位置中的堆叠追蹤结尾 ---
   于 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   于 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   于 FineCodeCoverage.Engine.ReportGenerator.ReportGeneratorUtil.<GenerateAsync>d__47.MoveNext()
--- 先前掷回例外状况之位置中的堆叠追蹤结尾 ---
   于 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   于 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   于 FineCodeCoverage.Engine.FCCEngine.<RunAndProcessReportAsync>d__38.MoveNext()
--- 先前掷回例外状况之位置中的堆叠追蹤结尾 ---
   于 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   于 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   于 FineCodeCoverage.Engine.FCCEngine.<>c__DisplayClass44_0.<<ReloadCoverage>b__0>d.MoveNext()
--- 先前掷回例外状况之位置中的堆叠追蹤结尾 ---
   于 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   于 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   于 FineCodeCoverage.Engine.FCCEngine.<>c__DisplayClass43_0.<<RunCancellableCoverageTask>b__1>d.MoveNext()

Log 里有出现了几个关键的讯息

CLR: Assert failure … !AreCetShadowStacksEnabled() || UseSpecialUserModeApc()
    File: …\threads.cpp, Line: 8329
    Image: …\coverlet.exe

这段错误讯息,其实并不是 xUnit 或 Fine Code Coverage 本身的问题,而是 .NET 执行阶段(CoreCLR)在载入 coverlet.exe 时,因为「Control-flow Enforcement Technology (CET)」硬体堆叠保护(Shadow Stacks)预设被启用,导致 profiler 注入机制不符合 CET 的安全要求而触发的。

简单来说,.NET 8/9 对 CET 的支援让 runtime 要求如果在硬体启用了 Shadow Stacks,就必须透过 UseSpecialUserModeApc 这类特殊程式框架才允许插入执行续上下文;而 Coverlet Console 现行版本并未呼叫到这些 API,于是就直接崩溃了。

  • Breaking change: CET supported by default - .NET | Microsoft Learn

网路上找寻各种资料,我有试着在测试专案的 csproj 档案里加入 <CETCompat>false</CETCompat>但似乎没有任何作用

  • dotnet 9 已知问题 默认开启 CET 导致进程崩溃

于是我採取的方式为「针对 coverlet.exe 关闭 CET」

以下的操作我是以 Windows 11 环境里操作的截图,而在 Windows 10 里面则是差不多(有部分功能显示名称会不同,但都可以对照)\

Windows 设定 →  隐私权与安全性 (Windows 安全性) →  应用程式与浏览器控制

Windows Security → App & browser control → Exploit protection → Exploit protection settings

Exploit protection → Program settings → Add program to customize → Chose exact file path

新增 coverlet.exe(路径为 …\FineCodeCoverage\coverlet\6.0.4\coverlet.console.6.0.4\coverlet.exe

一般路径位置会是 C:\Users\使用者名称(记得要改)\AppData\Local\FineCodeCoverage\coverlet\6.0.4\coverlet.console.6.0.4

然后开启的 Program settings: coverlet.exe  视窗里,将 Hardware-enforced Stack Protection 项目勾选 Override system settings,并将开关调整为 Off

最后按下 Apply  就可以。

 

Code Coverage 的限制与使用建议

  • 覆盖率指标:只是一种辅助工具,无法评估测试质量与完整度。
  • 测试设计优先:应依据需求分析、使用案例、边界与异常情境来撰写测试。
  • 不要作为 KPI 量化的数据依据:避免过度优化覆盖率而忽略测试逻辑与可维护性。

 

参考连结

  • 代码覆盖率 - Wikipedia
  • 什么是代码覆盖率? | Atlassian
  • 四种常见的程式码涵盖率  |  Articles  |  web.dev
  • 使用程式代码涵盖範围来判断要测试多少程序代码 | Microsoft Learn
  • 使用程式码涵盖範围进行单元测试 | Microsoft Learn
  • Fine Code Coverage - Visual Studio Marketplace
  • FortuneN/FineCodeCoverage: Visualize unit test code coverage easily for free in Visual Studio Community Edition (and other editions too)
  • Visual Studio Code Coverage with Fine Code Coverage Visual Studio 2022 Extension - CodeSloth
  • Mastering Code Coverage Analysis for .NET Projects without Breaking the Bank | NimblePros Blog
  • 通过 Coverlet + ReportGenerator + Fine Code Coverage 产生测试涵盖率报表 | 余小章 @ 大内殿堂 - 点部落

 

写这篇文章过程充满波折,历经多次自动储存失败、多次误触而关闭网页,再次开启编辑页面却发现里面的内容并不是上次所储存的版本,而是更早之前的内容,一堆已经写好的内容又只能重写…

以上

纯粹是在写兴趣的,用写程式、写文章来抒解工作压力

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章

5 点赞(415) 阅读(67)