鐵人賽系列文章導讀 — 重啟挑戰:老派軟體工程師的測試修練

2025 年 8 月到 9 月,我參加了 iThome 鐵人賽,花了 30 天寫完「重啟挑戰:老派軟體工程師的測試修練」這個系列。一直沒有在部落格這邊正式介紹過,趁這個機會寫一篇導讀,讓大家在還沒有把 30 篇全部看完也能瞭解裡面在講什麼。

30 天的內容從最基本的「為什麼要寫測試」一路寫到 Testcontainers、.NET Aspire 整合測試、TUnit,每一篇都有技術介紹說明、程式碼範例,以及我自己在專案裡踩過的坑。如果你對 .NET 測試有興趣但不確定要從哪裡開始看,這篇可以幫你省點時間。

另外,完賽之後我把這 30 天的測試知識重新整理成了 29 個 Agent Skills,讓 AI 可以直接拿來用。後續會有一系列文章介紹 `dotnet-testing-agent-skills` 這個專案 — 從 Agent Skills 到 Agent Orchestration 的完整方案。所以這篇鐵人賽導讀也算是後續系列的起點,先從源頭說起。

系列架構

iThome 2025 鐵人賽 30 天系列文章導讀

系列連結:https://ithelp.ithome.com.tw/users/20066083/ironman/8276

範例程式碼:https://github.com/kevintsengtw/30Days_in_Testing_Samples

30 天的文章我是按照學習順序安排的,從最基礎的觀念開始,一層一層往上疊加。大致分成七個階段:

階段天數主題
Day 01 ~ 03測試基礎與 xUnit
Day 04 ~ 06斷言與驗證
Day 07 ~ 09Mock 與依賴隔離
Day 10 ~ 15測試資料產生
Day 16 ~ 18特定場景處理
Day 19 ~ 25整合測試與容器化
Day 26 ~ 30新世代框架與 AI

前三個階段打底,中間兩個處理實務上一定會碰到的問題,後面兩個階段才進入整合測試和比較新的東西。不一定要從頭讀到尾,可以挑你現在最需要的階段直接看。


各篇導讀

第一階段:測試基礎與 xUnit(Day 01 ~ 03)

Day 01 — 老派工程師的測試啟蒙 - 為什麼我們需要測試?

從 AI 時代為什麼還要談測試切入,點出開發者用 AI 產生測試卻不理解內容的危機。文章涵蓋了測試金字塔、FIRST 原則、3A Pattern 和命名規範,後記裡提到了 2013 年在 twMVC 看到 91 哥的 TDD Living Demo,那是開始接觸測試的起點。

Day 02 — xUnit 框架深度解析 - 從生態概觀到實戰專案

比較了 xUnit、NUnit、MSTest 三家框架,講了從 MSTest 轉到 xUnit 的原因。文章涵蓋 [Fact][Theory]IClassFixture<T>ITestOutputHelper 等核心機制,也深入介紹了 xUnit 預設的測試隔離設計和各種 Assert 方法。

Day 03 — xUnit 進階功能與測試資料管理

深入 MemberDataClassDataPropertyData 等進階資料提供機制,介紹了 Test Data Builder Pattern 來管理測試資料,以及 IClassFixture<T>ICollectionFixture<T> 的資源共享策略。文章裡有提到不建議用 SQLite 做測試替代。

第二階段:斷言與驗證(Day 04 ~ 06)

Day 04 — AwesomeAssertions 基礎應用與實戰技巧

先分析了 FluentAssertions 從開源轉為商業授權的始末和對開發者的影響,再介紹 AwesomeAssertions 這個 Apache 2.0 的社群分支版本。涵蓋了物件、字串、數值、集合、例外、非同步等各種 Assertions 語法。

Day 05 — AwesomeAssertions 進階技巧與複雜情境應用

處理進階情境:Object Graph 的循環參照比對、非同步測試的執行時間驗證、自訂領域專屬 Assertions、動態排除時間戳記等自動生成欄位,以及大量資料的效能最佳化 Assertions 策略。

Day 06 — Code Coverage 程式碼涵蓋範圍實戰指南

介紹了 Code Coverage 的觀念和常見誤解,強調它不該拿來當 KPI。工具方面涵蓋了 Fine Code Coverage 擴充套件和 VS Code 的測試覆蓋率功能。後半段介紹了循環複雜度與測試案例數量的關係,以及怎麼從使用案例來決定該寫哪些測試。

第三階段:Mock 與依賴隔離(Day 07 ~ 09)

Day 07 — 依賴替代入門 - 使用 NSubstitute

先從檔案系統、時間、資料庫、Logger 四種常見的外部依賴問題切入,說明為什麼程式碼難測通常是設計問題而不是工具問題。介紹了 Test Double 的五種類型、Moq 的 SponsorLink 爭議事件,然後示範怎麼把一段不可測的程式碼重構為可注入依賴的設計,再用 NSubstitute 來寫測試。

Day 08 — 測試輸出與記錄 - xUnit ITestOutputHelper 與 ILogger

說明了 xUnit 的 ITestOutputHelper 怎麼用(包含生命週期管理和結構化輸出),重點處理了 ILogger 擴充方法無法直接 Mock 的問題。文章裡整理了 AbstractLogger、CompositeLogger、TestLogger 三種不同解法,分別適用於不同的驗證需求。

Day 09 — 測試私有與內部成員 - Private 與 Internal 的測試策略

討論了封裝原則和測試需求之間的平衡。Internal 成員介紹了 InternalsVisibleTo、csproj 設定、Meziantou 套件三種做法。Private 方法的部分除了 Reflection 之外,也介紹了用策略模式改善可測試性的替代設計,文章裡有提供完整的決策流程圖。

第四階段:測試資料產生(Day 10 ~ 15)

這個階段用了六天的篇幅,因為測試資料的準備在實務上很花時間,善用工具可以省下不少力氣。

Day 10 — AutoFixture 基礎:自動產生測試資料

從傳統測試資料準備的痛點切入,介紹 AutoFixture 的 fixture.Create<T>() 自動產生機制、複雜物件建構和循環參照處理。文章裡花了不少篇幅對比 Day 03 的 Test Data Builder Pattern 和 AutoFixture 的差異,各有適合的場景。

Day 11 — AutoFixture 進階:自訂化測試資料生成策略

深入 ISpecimenBuilder 的自訂機制,涵蓋 DataAnnotations 整合、屬性值範圍控制、.With() 固定值與動態值的差異。實作了自訂的 DateTime 和數值範圍 Builder,過程中解釋了 fixture.Customizations.Insert(0)Add() 的優先順序差異 — 這是數值型別自訂會失效的原因。

Day 12 — 結合 AutoData:xUnit 與 AutoFixture 的整合應用

介紹了 [AutoData][InlineAutoData][MemberAutoData] 讓測試參數自動產生的做法,包含 InlineAutoData 只能接受編譯時常數的限制。也涵蓋了從 CSV/JSON 外部檔案載入測試資料、自訂 CompositeAutoData 屬性和控制集合產生數量的技巧。

Day 13 — NSubstitute 與 AutoFixture 的整合應用

AutoNSubstituteCustomization 讓 AutoFixture 自動建立 Mock 物件,搭配 [Frozen] 確保注入的實例一致。實作範例裡刻意不 Mock IMapper(Mapster),而是使用真實的 Mapper 來驗證映射設定是否正確,屬於工具型依賴不需要 Mock 的設計考量。

Day 14 — Bogus 入門:與 AutoFixture 的差異比較

介紹了 Bogus 這個從 faker.js 移植過來的測試資料產生工具,涵蓋了內建的各種 DataSet、支援 40 多種語言(包含 zh_TW)、自訂 TaiwanDataSet。與 AutoFixture 做了深度比較:AutoFixture 擅長匿名測試、Bogus 擅長語意化資料,兩者可以互補,也介紹了 AutoBogus 這個整合方案。

Day 15 — AutoFixture 與 Bogus 的整合應用

整合 AutoFixture 和 Bogus,用自訂 ISpecimenBuilder 攔截屬性名稱(Email、Phone、Name 等)委託 Bogus 產生語意化資料。建立了統一的測試資料工廠和 TestBase 基底類別,也處理了循環參照和兩套工具的 Seed 管理問題。

第五階段:特定場景處理(Day 16 ~ 18)

這三天處理的都是實務上一定會碰到、但很多人不知道怎麼測的場景。

Day 16 — 測試日期與時間:Microsoft.Bcl.TimeProvider 取代 DateTime

直接呼叫 DateTime.Now 的程式碼無法控制測試時間,文章從不可預測性、邊界條件、並行競爭三個面向說明問題,再介紹 TimeProvider + FakeTimeProvider 的解法。實戰場景涵蓋排程觸發、快取過期、業務時間窗口的驗證,也整合了 AutoFixture 的 FakeTimeProviderCustomization

Day 17 — 檔案與 IO 測試:使用 System.IO.Abstractions 模擬檔案系統 - 實現可測試的檔案操作

靜態的 FileDirectory 在測試時有速度、環境依賴、並行衝突、錯誤模擬四個問題。用 IFileSystem + MockFileSystem 取代後,所有檔案操作都在記憶體裡跑。文章涵蓋了目錄操作、串流處理、設定檔管理器的完整範例,也示範了搭配 NSubstitute 模擬 IO 例外的做法。

Day 18 — 驗證測試:FluentValidation Test Extensions

比較了 DataAnnotation 和 FluentValidation 的差異,說明 DataAnnotation 在跨欄位驗證和條件式驗證的限制。用 TestValidate() 搭配 ShouldHaveValidationErrorFor() 可以精確驗證單一規則。進階部分整合了 FakeTimeProvider 做年齡驗證、NSubstitute 做非同步外部服務驗證。

第六階段:整合測試與容器化(Day 19 ~ 25)

這是整個系列篇幅最長的階段,花了七天。整合測試本來就比單元測試複雜,加上容器化的部分,要交代的細節不少。

Day 19 — 整合測試入門:基礎架構與應用場景

從整合測試的定義和測試金字塔中的定位開始,介紹 WebApplicationFactory<T> 在記憶體裡建立 TestServer 的做法。搭配 AwesomeAssertions.Web 驗證 HTTP 回應,其中 Satisfy<T>() 可以自動反序列化做型別驗證。文章整理了三個漸進式的學習層級和兩種測試策略。

Day 20 — Testcontainers 初探:使用 Docker 架設測試環境

先用大篇幅說明了 EF Core InMemory Database 在交易、LINQ 查詢、資料庫特定行為上的限制,接著介紹 Testcontainers 怎麼在測試裡啟動真正的 Docker 容器。涵蓋了 PostgreSQL、SQL Server、EF Core 的整合測試,以及用 WireMock 模擬 HTTP API 和 Redis 快取服務的測試。

Day 21 — Testcontainers 整合測試:MSSQL + EF Core 以及 Dapper 基礎應用

針對 Day 20 每個測試類別各建一個容器的效能問題,改用 Collection Fixture 共享 MSSQL 容器,啟動時間降了約 67%。介紹了 Repository Pattern 搭配介面分離原則的設計、SQL 指令碼外部化策略,同時展示了 EF Core(Include、AsSplitQuery、N+1 問題)和 Dapper(QueryMultiple、預存程序)兩種資料存取技術的測試實作。

Day 22 — Testcontainers 整合測試:MongoDB 及 Redis 基礎到進階

延續 Collection Fixture 模式,實作 MongoDB 和 Redis 的整合測試。MongoDB 涵蓋了 BSON 序列化、基礎 CRUD、索引效能測試。Redis 則完整測試了 String、Hash、List、Set、Sorted Set 五種資料結構,也處理了 Redis 容器的權限問題。

Day 23 — 整合測試實戰:WebApi 服務的整合測試

把前面幾天學的整合起來:Clean Architecture 搭配 PostgreSQL + Redis 雙容器。重點介紹了 ASP.NET Core 的 IExceptionHandler 錯誤處理模式(取代傳統 Middleware),以及 ProblemDetails 標準格式。用 Respawner 清測試資料,Flurl 建構查詢參數,完整實作了產品 CRUD 和健康檢查的測試。

Day 24 — .NET Aspire Testing 入門基礎介紹

介紹 .NET Aspire Testing 框架,透過 DistributedApplicationTestingBuilder 重用 AppHost 的設定來管理容器和連線字串。AppHost 專案是必要前提,不是可選項目。文章實作了 Repository 和 Service 層的整合測試,也記錄了容器啟動時間不一致、命名空間遺漏等實作過程中遇到的問題。

Day 25 — .NET Aspire 整合測試實戰:從 Testcontainers 到 .NET Aspire Testing

完整記錄了把 Day 23 的 Testcontainers 實作改寫為 .NET Aspire Testing 的遷移過程。解決了五個實際遇到的問題:Endpoint 設定衝突、PostgreSQL 資料庫建立、Respawn adapter 錯誤、Dapper 欄位映射(snake_case vs PascalCase)、時間依賴注入。也做了兩種方法的效能比較和選擇建議。

第七階段:新世代框架與 AI(Day 26 ~ 30)

最後五天的主題比較雜,但都是我覺得值得介紹的東西。

Day 26 — xUnit 升級指南:從 2.9.x 到 3.x 的轉換

整理了 xUnit v3 的破壞性變更(最低需求 .NET 8+、套件改名為 xunit.v3、測試專案要改為 Exe、IAsyncLifetime 行為變更等)和新功能(動態跳過 Assert.Skip()MatrixTheoryDataTestContext[Test] 屬性)。提供了從備份到逐步升級的完整步驟。

Day 27 — GitHub Copilot 測試實戰:AI 輔助測試開發指南

設計了三階段漸進式的 GitHub Copilot 測試練習:基礎業務邏輯(DiscountCalculator)、依賴注入(UserService)、業務流程(OrderProcessor)。透過 copilot-instructions.md 搭配三個通用指令範本(業務邏輯分析、測試情境設計、測試程式碼產生)來標準化 AI 輔助測試的流程。

Day 28 — TUnit 入門 - 下世代 .NET 測試框架探索

介紹 TUnit 這個用 Source Generator 取代 Runtime Reflection 的下世代測試框架,啟動速度比 xUnit 快了 23~54 倍。所有測試方法必須是 async Task、所有斷言都是非同步的。文章詳細對比了 TUnit 和 xUnit 的語法差異,也整理了評估框架是否適合導入的考量面向。

Day 29 — TUnit 進階應用:資料驅動測試與依賴注入深度實戰

介紹了 [MethodDataSource][ClassDataSource<T>][MatrixDataSource] 三種資料驅動測試方式,MatrixDataSource 要注意組合爆炸的問題。也涵蓋了 Properties 屬性標記和測試過濾、生命週期控制(建構式永遠比 TUnit 的 lifecycle attribute 先執行),以及 TUnit 原生的依賴注入機制。

Day 30 — TUnit 進階應用 - 執行控制與測試品質和 ASP.NET Core 整合測試實戰

涵蓋了 [Retry][Timeout][DisplayName] 等執行控制 Attribute,以及用 WebApplicationFactory 搭配 TUnit 做 ASP.NET Core 整合測試。進階部分示範了在 Assembly 層級用 Testcontainers 管理 PostgreSQL + Redis + Kafka 多容器環境,也解說了 TUnit 的 Source Generation 和 Reflection 兩種執行模式。

系列回顧

Day 31 - 重啟挑戰的測試修練總結:從基礎到實戰的 30 天回顧與 AI 時代的開發與測試模式轉變的想法

最後一篇分成四章:30 天回顧、個人推動測試文化的實務心得、AI 時代開發與測試模式轉變的想法、持續精進的方向。核心觀點是開發者要具備「測試識讀」的能力 — 自己得會寫測試,才能判斷 AI 產出的測試有沒有意義。


相關使用技術

整個系列用到的技術整理在這裡,方便查閱:

類別技術
測試框架xUnit 2.9.x / xUnit v3 / TUnit
斷言庫AwesomeAssertions(FluentAssertions 的 Apache 2.0 fork)
MockNSubstitute
測試資料AutoFixture、Bogus、AutoBogus
時間測試Microsoft.Bcl.TimeProvider + FakeTimeProvider
檔案測試System.IO.Abstractions(IFileSystem + MockFileSystem)
驗證測試FluentValidation + Test Extensions
整合測試WebApplicationFactory、Testcontainers、.NET Aspire Testing
HTTPAwesomeAssertions.Web、Flurl
覆蓋率Fine Code Coverage、dotnet-coverage
AI 輔助GitHub Copilot + copilot-instructions.md
容器Docker(PostgreSQL、MSSQL、MongoDB、Redis、WireMock、Kafka)

文章及範例連結

「2025 iThome 鐵人賽 - 重啟挑戰:老派軟體工程師的測試修練」

Day主題文章連結程式碼
01老派工程師的測試啟蒙 - 為什麼我們需要測試?連結day01
02xUnit 框架深度解析 - 從生態概觀到實戰專案連結day02
03xUnit 進階功能與測試資料管理連結day03
04AwesomeAssertions 基礎應用與實戰技巧連結day04
05AwesomeAssertions 進階技巧與複雜情境應用連結day05
06Code Coverage 程式碼涵蓋範圍實戰指南連結-
07依賴替代入門 - 使用 NSubstitute連結day07
08測試輸出與記錄 - xUnit ITestOutputHelper 與 ILogger連結day08
09測試私有與內部成員 - Private 與 Internal 的測試策略連結day09
10AutoFixture 基礎:自動產生測試資料連結day10
11AutoFixture 進階:自訂化測試資料生成策略連結day11
12結合 AutoData:xUnit 與 AutoFixture 的整合應用連結day12
13NSubstitute 與 AutoFixture 的整合應用連結day13
14Bogus 入門:與 AutoFixture 的差異比較連結day14
15AutoFixture 與 Bogus 的整合應用連結day15
16測試日期與時間:Microsoft.Bcl.TimeProvider 取代 DateTime連結day16
17檔案與 IO 測試:使用 System.IO.Abstractions 模擬檔案系統 - 實現可測試的檔案操作連結day17
18驗證測試:FluentValidation Test Extensions連結day18
19整合測試入門:基礎架構與應用場景連結day19
20Testcontainers 初探:使用 Docker 架設測試環境連結day20
21Testcontainers 整合測試:MSSQL + EF Core 以及 Dapper 基礎應用連結day21
22Testcontainers 整合測試:MongoDB 及 Redis 基礎到進階連結day22
23整合測試實戰:WebApi 服務的整合測試連結day23
24.NET Aspire Testing 入門基礎介紹連結day24
25.NET Aspire 整合測試實戰:從 Testcontainers 到 .NET Aspire Testing連結day25
26xUnit 升級指南:從 2.9.x 到 3.x 的轉換連結day26
27GitHub Copilot 測試實戰:AI 輔助測試開發指南連結day27
28TUnit 入門 - 下世代 .NET 測試框架探索連結day28
29TUnit 進階應用:資料驅動測試與依賴注入深度實戰連結day29
30TUnit 進階應用 - 執行控制與測試品質和 ASP.NET Core 整合測試實戰連結day30
31重啟挑戰的測試修練總結:從基礎到實戰的 30 天回顧與 AI 時代的開發與測試模式轉變的想法連結-

最後

其實一直很低調的是... 這個系列得到了 Software Development 組的冠軍

2025 iThome 鐵人賽 Software Development 組冠軍得獎感言

首先,要感謝 iThome 主辦單位以及所有的評審委員,給予我這個獎項。

說實話,今天能站在這裡,我感到非常意外,也非常的驚喜。今年 Software Development 組的參賽作品都相當優秀,技術含金量都很高。我當初參賽的初衷,真的就只是想把文章寫完,完全沒有想過會得獎,更沒想過能幸運地拿到冠軍。

這次我的參賽主題是**《重啟挑戰:老派軟體工程師的測試修練》**。

為什麼會說是「老派」?又為什麼要寫這系列?

其實這源自於我對這幾年 AI 技術飛速發展的一種「焦慮」與「反思」。我發現,隨著我們越來越習慣依賴 AI 來寫程式,那些我們曾經爛熟於心的技術細節,似乎正在慢慢被遺忘。

所以我告訴自己,必須要把這些我所熟悉的技術與知識保留下來。就像《哈利波特》裡的鄧不利多,把繁雜的思緒抽取出來放進「集思瓶(Pensieve)」一樣。這三十天的文章,就是我的集思瓶,我希望透過文字,將那些屬於工程師的邏輯與靈魂,完整地封存下來。

在學習單元測試這條路上,我並不是獨行俠。我要特別感謝幾位前輩:

一位是 Joey Chen 91 哥,另一位是 twMVC 與 SkillTree 和 dotblogs 點部落維護者的 Demo Fan。

很榮幸頒獎者是 dotblogs 創辦人 游舒帆 gipi 老師。

是你們帶領我進入測試與開發社群和技術分享的領域,沒有你們過去的指導與啟發和帶領,就沒有今天這系列文章的誕生。

最後,我想將這個獎項分享給所有的開發者。

我們身處在一個 AI 時代,寫程式變得前所未有的容易,但我期許我們每一個人,在善用工具的同時,永遠不要放棄學習,也不要停止對技術本質的追求與進步。

謝謝大家!

純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力