---
name: dotnet-testing-advanced-tunit-fundamentals
description: |
TUnit 新世代測試框架入門完整指南。
涵蓋 Source Generator 驅動測試發現、AOT 編譯支援、流暢式非同步斷言。
包含專案建立、[Test] 屬性、生命週期管理、並行控制與 xUnit 語法對照。
triggers:
# 核心關鍵字
- TUnit
- tunit testing
- source generator testing
- AOT testing
- 新世代測試框架
# 技術術語
- [Test]
- [Arguments]
- TUnit.Assertions
- Assert.That().IsEqualTo
- await Assert.That
- async Task test
# 生命週期
- [Before(Test)]
- [Before(Class)]
- [After(Test)]
- [After(Class)]
- NotInParallel
# 使用情境
- TUnit.Templates
- Microsoft.Testing.Platform
- PublishAot
- TUnit vs xUnit
- 並行執行
- Source Generated
license: MIT
metadata:
author: Kevin Tseng
version: "1.0.0"
tags: "tunit, testing-framework, source-generator, aot, modern-testing, performance"
---
# TUnit 新世代測試框架入門基礎
## 技能概述
本技能涵蓋 TUnit 新世代 .NET 測試框架的入門基礎,從框架特色到實際專案建立與測試撰寫。
**核心主題:**
- TUnit 框架特色與設計理念
- Source Generator 驅動的測試發現
- AOT (Ahead-of-Time) 編譯支援
- 流暢式非同步斷言系統
- 專案建立與套件配置
- 與 xUnit 的語法差異比較
---
## TUnit 框架核心特色
### 1. Source Generator 驅動的測試發現
TUnit 與傳統測試框架最大的差異在於使用 Source Generator 在**編譯時期**完成測試發現:
**傳統框架的方式(xUnit):**
```csharp
// xUnit 在執行時期透過反射掃描所有方法
public class TraditionalTests
{
[Fact] // 執行時期才被發現
public void TestMethod() { }
}
```
**TUnit 的創新做法:**
```csharp
// TUnit 在編譯時期就透過 Source Generator 產生測試註冊程式碼
public class ModernTests
{
[Test] // 編譯時期就被處理和最佳化
public async Task TestMethod()
{
await Assert.That(true).IsTrue();
}
}
```
**優勢:**
1. 避免反射成本:所有測試發現在編譯時期完成
2. AOT 相容:完全支援 Native AOT 編譯
3. 更快的啟動時間:特別是在大型測試專案中
### 2. AOT (Ahead-of-Time) 編譯支援
**JIT vs AOT 編譯流程:**
```text
傳統 JIT:C# 原始碼 → IL 中間碼 → 執行時期 JIT 編譯 → 機器碼 → 執行
AOT: C# 原始碼 → 編譯時期直接產生 → 機器碼 → 直接執行
```
**AOT 編譯的優勢:**
- 超快啟動時間(無需等待 JIT 編譯)
- 更小的記憶體占用
- 可預測的效能
- 更適合容器化部署
**啟用 AOT 支援:**
```xml
true
true
```
**實際效能差異:**
```text
傳統 JIT 編譯測試啟動時間:約 1-2 秒
TUnit AOT 編譯測試啟動時間:約 50-100 毫秒
(大型專案可達 10-30 倍啟動時間改善)
```
### 3. Microsoft.Testing.Platform 採用
TUnit 建構在微軟最新的 Microsoft.Testing.Platform 之上,而非傳統的 VSTest 平台:
- 更輕量的測試執行器
- 更好的並行控制機制
- 原生支援最新的 IDE 整合
**重要注意事項:**
TUnit 專案**不需要**也**不應該**安裝 `Microsoft.NET.Test.Sdk` 套件。
### 4. 預設並行執行
TUnit 將並行執行設為預設,並提供精細的控制:
```csharp
// 預設所有測試都會並行執行
[Test]
public async Task ParallelTest1() { }
[Test]
public async Task ParallelTest2() { }
// 需要時可以控制並行行為
[Test]
[NotInParallel("DatabaseTests")]
public async Task DatabaseTest() { }
```
---
## TUnit 專案建立
### 方式一:手動建立(理解底層架構)
```bash
# 建立專案目錄
mkdir TUnitDemo
cd TUnitDemo
# 建立解決方案
dotnet new sln -n MyApp
# 建立主專案
dotnet new classlib -n MyApp.Core -o src/MyApp.Core
# 建立測試專案(使用 console 模板)
dotnet new console -n MyApp.Tests -o tests/MyApp.Tests
# 加入解決方案
dotnet sln add src/MyApp.Core/MyApp.Core.csproj
dotnet sln add tests/MyApp.Tests/MyApp.Tests.csproj
# 加入專案參考
dotnet add tests/MyApp.Tests/MyApp.Tests.csproj reference src/MyApp.Core/MyApp.Core.csproj
```
### 方式二:使用 TUnit Template(推薦)
```bash
# 安裝 TUnit 專案模板
dotnet new install TUnit.Templates
# 使用 TUnit template 建立測試專案
dotnet new tunit -n MyApp.Tests -o tests/MyApp.Tests
```
### 測試專案 csproj 設定
```xml
net9.0
enable
enable
false
true
```
### GlobalUsings 設定
```csharp
// GlobalUsings.cs
global using TUnit.Core;
global using TUnit.Assertions;
global using MyApp.Core;
```
---
## 非同步測試方法(必要)
TUnit 的**所有測試方法都必須是非同步的**,這是框架的技術要求:
```csharp
// ❌ 錯誤:無法編譯
[Test]
public void WrongTest()
{
Assert.That(1 + 1).IsEqualTo(2);
}
// ✅ 正確:使用 async Task
[Test]
public async Task CorrectTest()
{
await Assert.That(1 + 1).IsEqualTo(2);
}
```
---
## 測試屬性與參數化
### 基本測試 [Test]
TUnit 統一使用 `[Test]` 屬性,不像 xUnit 區分 `[Fact]` 和 `[Theory]`:
```csharp
// TUnit:統一使用 [Test]
[Test]
public async Task Add_輸入1和2_應回傳3()
{
var calculator = new Calculator();
var result = calculator.Add(1, 2);
await Assert.That(result).IsEqualTo(3);
}
```
### 參數化測試 [Arguments]
```csharp
// TUnit:使用 [Arguments](相當於 xUnit 的 [InlineData])
[Test]
[Arguments(1, 2, 3)]
[Arguments(-1, 1, 0)]
[Arguments(0, 0, 0)]
[Arguments(100, -50, 50)]
public async Task Add_多組輸入_應回傳正確結果(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
await Assert.That(result).IsEqualTo(expected);
}
```
---
## TUnit.Assertions 斷言系統
TUnit 採用流暢式(Fluent)斷言設計,所有斷言都是非同步的:
### 基本相等性斷言
```csharp
[Test]
public async Task 基本相等性斷言範例()
{
var expected = 42;
var actual = 40 + 2;
await Assert.That(actual).IsEqualTo(expected);
await Assert.That(actual).IsNotEqualTo(43);
// Null 檢查
string? nullValue = null;
await Assert.That(nullValue).IsNull();
await Assert.That("test").IsNotNull();
}
```
### 布林值斷言
```csharp
[Test]
public async Task 布林值斷言範例()
{
var condition = 1 + 1 == 2;
await Assert.That(condition).IsTrue();
await Assert.That(1 + 1 == 3).IsFalse();
var number = 10;
await Assert.That(number > 5).IsTrue();
}
```
### 數值比較斷言
```csharp
[Test]
public async Task 數值比較斷言範例()
{
var actual = 10;
await Assert.That(actual).IsGreaterThan(5);
await Assert.That(actual).IsGreaterThanOrEqualTo(10);
await Assert.That(actual).IsLessThan(15);
await Assert.That(actual).IsBetween(5, 15);
}
[Test]
[Arguments(3.14159, 3.14, 0.01)]
public async Task 浮點數精確度控制(double actual, double expected, double tolerance)
{
await Assert.That(actual)
.IsEqualTo(expected)
.Within(tolerance);
}
```
### 字串斷言
```csharp
[Test]
public async Task 字串斷言範例()
{
var email = "user@example.com";
await Assert.That(email).Contains("@");
await Assert.That(email).StartsWith("user");
await Assert.That(email).EndsWith(".com");
await Assert.That(email).DoesNotContain(" ");
await Assert.That("").IsEmpty();
await Assert.That(email).IsNotEmpty();
}
```
### 集合斷言
```csharp
[Test]
public async Task 集合斷言範例()
{
var numbers = new List { 1, 2, 3, 4, 5 };
await Assert.That(numbers).HasCount(5);
await Assert.That(numbers).IsNotEmpty();
await Assert.That(numbers).Contains(3);
await Assert.That(numbers).DoesNotContain(10);
await Assert.That(numbers.First()).IsEqualTo(1);
await Assert.That(numbers.Last()).IsEqualTo(5);
}
```
### 例外斷言
```csharp
[Test]
public async Task 例外斷言範例()
{
var calculator = new Calculator();
// 檢查特定例外類型
await Assert.That(() => calculator.Divide(10, 0))
.Throws();
// 檢查例外訊息
await Assert.That(() => calculator.Divide(10, 0))
.Throws()
.WithMessage("除數不能為零");
// 檢查不拋出例外
await Assert.That(() => calculator.Add(1, 2))
.DoesNotThrow();
}
```
### And / Or 條件組合
```csharp
[Test]
public async Task 條件組合範例()
{
var number = 10;
// And:所有條件都必須成立
await Assert.That(number)
.IsGreaterThan(5)
.And.IsLessThan(15)
.And.IsEqualTo(10);
// Or:任一條件成立即可
await Assert.That(number)
.IsEqualTo(5)
.Or.IsEqualTo(10)
.Or.IsEqualTo(15);
}
```
---
## 測試生命週期管理
### 建構式與 Dispose 模式
```csharp
public class BasicLifecycleTests : IDisposable
{
private readonly Calculator _calculator;
public BasicLifecycleTests()
{
_calculator = new Calculator();
}
[Test]
public async Task Add_基本測試()
{
await Assert.That(_calculator.Add(1, 2)).IsEqualTo(3);
}
public void Dispose()
{
// 清理資源
}
}
```
### Before / After 屬性
TUnit 提供更細緻的生命週期控制:
```csharp
public class LifecycleTests
{
private static TestDatabase? _database;
// 類別層級:所有測試執行前只執行一次
[Before(Class)]
public static async Task ClassSetup()
{
_database = new TestDatabase();
await _database.InitializeAsync();
}
// 測試層級:每個測試執行前都會執行
[Before(Test)]
public async Task TestSetup()
{
await _database!.ClearDataAsync();
}
[Test]
public async Task 測試使用者建立()
{
var userService = new UserService(_database!);
var user = await userService.CreateUserAsync("test@example.com");
await Assert.That(user.Id).IsNotEqualTo(Guid.Empty);
}
// 測試層級:每個測試執行後都會執行
[After(Test)]
public async Task TestTearDown()
{
// 記錄測試結果
}
// 類別層級:所有測試執行後只執行一次
[After(Class)]
public static async Task ClassTearDown()
{
if (_database != null)
{
await _database.DisposeAsync();
}
}
}
```
### 生命週期屬性種類
| 屬性 | 類型 | 說明 |
| -------------------- | -------- | ------------------------ |
| `[Before(Test)]` | 實例方法 | 每個測試執行前 |
| `[Before(Class)]` | 靜態方法 | 類別中第一個測試執行前 |
| `[Before(Assembly)]` | 靜態方法 | 組件中第一個測試執行前 |
| `[After(Test)]` | 實例方法 | 每個測試執行後 |
| `[After(Class)]` | 靜態方法 | 類別中最後一個測試執行後 |
| `[After(Assembly)]` | 靜態方法 | 組件中最後一個測試執行後 |
### 執行順序
```text
1. Before(Class)
2. 建構式
3. Before(Test)
4. 測試方法
5. After(Test)
6. Dispose
7. After(Class)
```
---
## 並行執行控制
### NotInParallel 屬性
```csharp
// 預設並行執行
[Test]
public async Task 並行測試1() { }
[Test]
public async Task 並行測試2() { }
// 控制特定測試不要並行
[Test]
[NotInParallel("DatabaseTests")]
public async Task 資料庫測試1_不並行執行()
{
// 這個測試不會與其他 "DatabaseTests" 群組並行執行
}
[Test]
[NotInParallel("DatabaseTests")]
public async Task 資料庫測試2_不並行執行()
{
// 與資料庫測試1 依序執行
}
```
---
## xUnit 與 TUnit 語法對照
| 功能 | xUnit | TUnit |
| -------------- | ------------------------------------- | ------------------------------------------------ |
| **基本測試** | `[Fact]` | `[Test]` |
| **參數化測試** | `[Theory]` + `[InlineData]` | `[Test]` + `[Arguments]` |
| **基本斷言** | `Assert.Equal(expected, actual)` | `await Assert.That(actual).IsEqualTo(expected)` |
| **布林斷言** | `Assert.True(condition)` | `await Assert.That(condition).IsTrue()` |
| **例外測試** | `Assert.Throws(() => action())` | `await Assert.That(() => action()).Throws()` |
| **Null 檢查** | `Assert.Null(value)` | `await Assert.That(value).IsNull()` |
| **字串檢查** | `Assert.Contains("text", fullString)` | `await Assert.That(fullString).Contains("text")` |
### 遷移範例
**xUnit 原始程式碼:**
```csharp
[Theory]
[InlineData("test@example.com", true)]
[InlineData("invalid", false)]
public void IsValidEmail_各種輸入_應回傳正確驗證結果(string email, bool expected)
{
var result = _validator.IsValidEmail(email);
Assert.Equal(expected, result);
}
```
**TUnit 轉換後:**
```csharp
[Test]
[Arguments("test@example.com", true)]
[Arguments("invalid", false)]
public async Task IsValidEmail_各種輸入_應回傳正確驗證結果(string email, bool expected)
{
var result = _validator.IsValidEmail(email);
await Assert.That(result).IsEqualTo(expected);
}
```
**主要變更:**
1. `[Theory]` → `[Test]`
2. `[InlineData]` → `[Arguments]`
3. 方法改為 `async Task`
4. 所有斷言加上 `await`
5. 流暢式斷言語法
---
## 執行與偵錯
### CLI 執行
```bash
# 建置專案
dotnet build
# 執行所有測試
dotnet test
# 詳細輸出
dotnet test --verbosity normal
# 產生覆蓋率報告
dotnet test --coverage
# 過濾特定測試
dotnet test --filter "ClassName=CalculatorTests"
dotnet test --filter "TestName~Add"
```
### AOT 編譯執行
```bash
# 發佈為 AOT 編譯版本
dotnet publish -c Release -p:PublishAot=true
# 執行 AOT 編譯的測試
./bin/Release/net9.0/publish/MyApp.Tests.exe
```
### IDE 整合
**Visual Studio 2022:**
- 版本需 17.13+
- 啟用 "Use testing platform server mode"
**VS Code:**
- 安裝 C# Dev Kit 擴充套件
- 啟用 "Use Testing Platform Protocol"
**JetBrains Rider:**
- 啟用 "Testing Platform support"
---
## 效能比較
| 場景 | xUnit | TUnit | TUnit AOT | 效能提升 |
| ---------------- | ------- | ------- | --------- | --------- |
| **簡單測試執行** | 1,400ms | 1,000ms | 60ms | 23x (AOT) |
| **非同步測試** | 1,400ms | 930ms | 26ms | 54x (AOT) |
| **並行測試** | 1,425ms | 999ms | 54ms | 26x (AOT) |
---
## 常見問題與解決方案
### 問題 1:套件相容性
**錯誤:** 安裝了 `Microsoft.NET.Test.Sdk` 導致測試無法發現
**解決方案:** 移除 `Microsoft.NET.Test.Sdk`,TUnit 使用新的測試平台
### 問題 2:IDE 整合問題
**症狀:** 測試在 IDE 中無法顯示或執行
**解決方案:**
1. 確認 IDE 版本支援 Microsoft.Testing.Platform
2. 啟用相關預覽功能
3. 重新載入專案或重啟 IDE
### 問題 3:非同步斷言遺忘
**症狀:** 編譯錯誤或斷言無法正常執行
**解決方案:** 所有斷言都需要 `await`,測試方法必須是 `async Task`
---
## 適用場景評估
### 適合使用 TUnit
1. **全新專案**:沒有歷史包袱
2. **效能要求高**:大型測試套件(1000+ 測試)
3. **技術棧先進**:使用 .NET 8+,計劃採用 AOT
4. **CI/CD 重度使用**:測試執行時間直接影響部署頻率
5. **容器化部署**:快速啟動時間很重要
### 暫時不建議
1. **Legacy 專案**:已有大量 xUnit 測試
2. **保守團隊**:需要穩定性勝過創新性
3. **複雜測試生態**:大量使用 xUnit 特定套件
4. **舊版 .NET**:還在 .NET 6/7
---
## 參考資源
### 原始文章
本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
- **Day 28 - TUnit 入門 - 下世代 .NET 測試框架探索**
- 鐵人賽文章:https://ithelp.ithome.com.tw/articles/10377828
- 範例程式碼:https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day28
### 官方資源
- [TUnit 官方網站](https://tunit.dev/)
- [TUnit GitHub](https://github.com/thomhurst/TUnit)
- [從 xUnit 遷移指南](https://tunit.dev/docs/migration/xunit)
### Microsoft 官方文件
- [Microsoft.Testing.Platform 介紹](https://learn.microsoft.com/dotnet/core/testing/microsoft-testing-platform-intro)
- [原生 AOT 部署](https://learn.microsoft.com/zh-tw/dotnet/core/deploying/native-aot)