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 ~ 09 | Mock 與依賴隔離 |
| 四 | 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 進階功能與測試資料管理
深入 MemberData、ClassData、PropertyData 等進階資料提供機制,介紹了 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 模擬檔案系統 - 實現可測試的檔案操作
靜態的 File、Directory 在測試時有速度、環境依賴、並行衝突、錯誤模擬四個問題。用 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()、MatrixTheoryData、TestContext、[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) |
| Mock | NSubstitute |
| 測試資料 | AutoFixture、Bogus、AutoBogus |
| 時間測試 | Microsoft.Bcl.TimeProvider + FakeTimeProvider |
| 檔案測試 | System.IO.Abstractions(IFileSystem + MockFileSystem) |
| 驗證測試 | FluentValidation + Test Extensions |
| 整合測試 | WebApplicationFactory、Testcontainers、.NET Aspire Testing |
| HTTP | AwesomeAssertions.Web、Flurl |
| 覆蓋率 | Fine Code Coverage、dotnet-coverage |
| AI 輔助 | GitHub Copilot + copilot-instructions.md |
| 容器 | Docker(PostgreSQL、MSSQL、MongoDB、Redis、WireMock、Kafka) |
文章及範例連結
「2025 iThome 鐵人賽 - 重啟挑戰:老派軟體工程師的測試修練」
- 系列文章連結:https://ithelp.ithome.com.tw/users/20066083/ironman/8276
- 範例程式碼:https://github.com/kevintsengtw/30Days_in_Testing_Samples
| Day | 主題 | 文章連結 | 程式碼 |
|---|---|---|---|
| 01 | 老派工程師的測試啟蒙 - 為什麼我們需要測試? | 連結 | day01 |
| 02 | xUnit 框架深度解析 - 從生態概觀到實戰專案 | 連結 | day02 |
| 03 | xUnit 進階功能與測試資料管理 | 連結 | day03 |
| 04 | AwesomeAssertions 基礎應用與實戰技巧 | 連結 | day04 |
| 05 | AwesomeAssertions 進階技巧與複雜情境應用 | 連結 | day05 |
| 06 | Code Coverage 程式碼涵蓋範圍實戰指南 | 連結 | - |
| 07 | 依賴替代入門 - 使用 NSubstitute | 連結 | day07 |
| 08 | 測試輸出與記錄 - xUnit ITestOutputHelper 與 ILogger | 連結 | day08 |
| 09 | 測試私有與內部成員 - Private 與 Internal 的測試策略 | 連結 | day09 |
| 10 | AutoFixture 基礎:自動產生測試資料 | 連結 | day10 |
| 11 | AutoFixture 進階:自訂化測試資料生成策略 | 連結 | day11 |
| 12 | 結合 AutoData:xUnit 與 AutoFixture 的整合應用 | 連結 | day12 |
| 13 | NSubstitute 與 AutoFixture 的整合應用 | 連結 | day13 |
| 14 | Bogus 入門:與 AutoFixture 的差異比較 | 連結 | day14 |
| 15 | AutoFixture 與 Bogus 的整合應用 | 連結 | day15 |
| 16 | 測試日期與時間:Microsoft.Bcl.TimeProvider 取代 DateTime | 連結 | day16 |
| 17 | 檔案與 IO 測試:使用 System.IO.Abstractions 模擬檔案系統 - 實現可測試的檔案操作 | 連結 | day17 |
| 18 | 驗證測試:FluentValidation Test Extensions | 連結 | day18 |
| 19 | 整合測試入門:基礎架構與應用場景 | 連結 | day19 |
| 20 | Testcontainers 初探:使用 Docker 架設測試環境 | 連結 | day20 |
| 21 | Testcontainers 整合測試:MSSQL + EF Core 以及 Dapper 基礎應用 | 連結 | day21 |
| 22 | Testcontainers 整合測試:MongoDB 及 Redis 基礎到進階 | 連結 | day22 |
| 23 | 整合測試實戰:WebApi 服務的整合測試 | 連結 | day23 |
| 24 | .NET Aspire Testing 入門基礎介紹 | 連結 | day24 |
| 25 | .NET Aspire 整合測試實戰:從 Testcontainers 到 .NET Aspire Testing | 連結 | day25 |
| 26 | xUnit 升級指南:從 2.9.x 到 3.x 的轉換 | 連結 | day26 |
| 27 | GitHub Copilot 測試實戰:AI 輔助測試開發指南 | 連結 | day27 |
| 28 | TUnit 入門 - 下世代 .NET 測試框架探索 | 連結 | day28 |
| 29 | TUnit 進階應用:資料驅動測試與依賴注入深度實戰 | 連結 | day29 |
| 30 | TUnit 進階應用 - 執行控制與測試品質和 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 時代,寫程式變得前所未有的容易,但我期許我們每一個人,在善用工具的同時,永遠不要放棄學習,也不要停止對技術本質的追求與進步。
謝謝大家!
純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力