這是 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):測試執行時,有被呼叫到的方法(或函式)數 ÷ 總方法數
雖然覆蓋率指標有助於快速發現未被測試的程式區段,但它無法評估測試本身的品質與完整度;測試邊界、異常流程、業務情境等仍需依需求與案例分析。
相關連結:
在「使用程式代碼涵蓋範圍來判斷要測試多少程序代碼 | Microsoft Learn」這篇文章裡就有提到:
若要判斷專案程式碼中經由測試的部分比例,例如單元測試,您可以使用 Visual Studio 的程式碼覆蓋率功能。 若要有效防範程式錯誤,您的測試應該執行或「涵蓋」大部分的程式碼。
但是在 VIsual Studio 裡只有 Enterprise 這個版本才有內建程式碼覆蓋範圍的功能

雖然我們也可以在 CLI 裡使用 dotnet-coverage 工具來取得程式碼覆蓋範圍的數據並且使用 Coverlet 與 ReportGenerator 產生報表,

但是沒有整合在 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 不會等到測試全部結束,會在測試執行的同時,就平行啟動覆蓋率工具收集資料,通常可以縮短整體收集時間,但某些情境下(如工具注入延遲)可能會導致少數測試結果遺漏。

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\xxxxyyyy\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\xxxxyyyy\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,於是就直接崩潰了。
網路上找尋各種資料,我有試著在測試專案的 csproj 檔案裡加入 <CETCompat>false</CETCompat>
但似乎沒有任何作用
於是我採取的方式為「針對 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 產生測試涵蓋率報表 | 余小章 @ 大內殿堂 - 點部落
寫這篇文章過程充滿波折,歷經多次自動儲存失敗、多次誤觸而關閉網頁,再次開啟編輯頁面卻發現裡面的內容並不是上次所儲存的版本,而是更早之前的內容,一堆已經寫好的內容又只能重寫…
以上
純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力