dotnet-testing-advanced-tunit-fundamentals
SKILL.md
TUnit 新世代測試框架入門基礎
技能概述
本技能涵蓋 TUnit 新世代 .NET 測試框架的入門基礎,從框架特色到實際專案建立與測試撰寫。
核心主題:
- TUnit 框架特色與設計理念
- Source Generator 驅動的測試發現
- AOT (Ahead-of-Time) 編譯支援
- 流暢式非同步斷言系統
- 專案建立與套件配置
- 與 xUnit 的語法差異比較
TUnit 框架核心特色
1. Source Generator 驅動的測試發現
TUnit 與傳統測試框架最大的差異在於使用 Source Generator 在編譯時期完成測試發現:
傳統框架的方式(xUnit):
// xUnit 在執行時期透過反射掃描所有方法
public class TraditionalTests
{
[Fact] // 執行時期才被發現
public void TestMethod() { }
}
TUnit 的創新做法:
// TUnit 在編譯時期就透過 Source Generator 產生測試註冊程式碼
public class ModernTests
{
[Test] // 編譯時期就被處理和最佳化
public async Task TestMethod()
{
await Assert.That(true).IsTrue();
}
}
優勢:
- 避免反射成本:所有測試發現在編譯時期完成
- AOT 相容:完全支援 Native AOT 編譯
- 更快的啟動時間:特別是在大型測試專案中
2. AOT (Ahead-of-Time) 編譯支援
JIT vs AOT 編譯流程:
傳統 JIT:C# 原始碼 → IL 中間碼 → 執行時期 JIT 編譯 → 機器碼 → 執行
AOT: C# 原始碼 → 編譯時期直接產生 → 機器碼 → 直接執行
AOT 編譯的優勢:
- 超快啟動時間(無需等待 JIT 編譯)
- 更小的記憶體占用
- 可預測的效能
- 更適合容器化部署
啟用 AOT 支援:
<PropertyGroup>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
實際效能差異:
傳統 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 將並行執行設為預設,並提供精細的控制:
// 預設所有測試都會並行執行
[Test]
public async Task ParallelTest1() { }
[Test]
public async Task ParallelTest2() { }
// 需要時可以控制並行行為
[Test]
[NotInParallel("DatabaseTests")]
public async Task DatabaseTest() { }
TUnit 專案建立
方式一:手動建立(理解底層架構)
# 建立專案目錄
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(推薦)
# 安裝 TUnit 專案模板
dotnet new install TUnit.Templates
# 使用 TUnit template 建立測試專案
dotnet new tunit -n MyApp.Tests -o tests/MyApp.Tests
測試專案 csproj 設定
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<!-- TUnit 核心套件 -->
<PackageReference Include="TUnit" Version="0.57.24" />
<!-- 程式碼覆蓋率支援 -->
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" Version="17.12.4" />
<!-- TRX 報告支援 -->
<PackageReference Include="Microsoft.Testing.Extensions.TrxReport" Version="1.4.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MyApp.Core\MyApp.Core.csproj" />
</ItemGroup>
</Project>
GlobalUsings 設定
// GlobalUsings.cs
global using TUnit.Core;
global using TUnit.Assertions;
global using MyApp.Core;
非同步測試方法(必要)
TUnit 的所有測試方法都必須是非同步的,這是框架的技術要求:
// ❌ 錯誤:無法編譯
[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]:
// 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]
// 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)斷言設計,所有斷言都是非同步的:
基本相等性斷言
[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();
}
布林值斷言
[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();
}
數值比較斷言
[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);
}
字串斷言
[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();
}
集合斷言
[Test]
public async Task 集合斷言範例()
{
var numbers = new List<int> { 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);
}
例外斷言
[Test]
public async Task 例外斷言範例()
{
var calculator = new Calculator();
// 檢查特定例外類型
await Assert.That(() => calculator.Divide(10, 0))
.Throws<DivideByZeroException>();
// 檢查例外訊息
await Assert.That(() => calculator.Divide(10, 0))
.Throws<DivideByZeroException>()
.WithMessage("除數不能為零");
// 檢查不拋出例外
await Assert.That(() => calculator.Add(1, 2))
.DoesNotThrow();
}
And / Or 條件組合
[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 模式
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 提供更細緻的生命週期控制:
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)] |
靜態方法 | 組件中最後一個測試執行後 |
執行順序
1. Before(Class)
2. 建構式
3. Before(Test)
4. 測試方法
5. After(Test)
6. Dispose
7. After(Class)
並行執行控制
NotInParallel 屬性
// 預設並行執行
[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<T>(() => action()) |
await Assert.That(() => action()).Throws<T>() |
| Null 檢查 | Assert.Null(value) |
await Assert.That(value).IsNull() |
| 字串檢查 | Assert.Contains("text", fullString) |
await Assert.That(fullString).Contains("text") |
遷移範例
xUnit 原始程式碼:
[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 轉換後:
[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);
}
主要變更:
[Theory]→[Test][InlineData]→[Arguments]- 方法改為
async Task - 所有斷言加上
await - 流暢式斷言語法
執行與偵錯
CLI 執行
# 建置專案
dotnet build
# 執行所有測試
dotnet test
# 詳細輸出
dotnet test --verbosity normal
# 產生覆蓋率報告
dotnet test --coverage
# 過濾特定測試
dotnet test --filter "ClassName=CalculatorTests"
dotnet test --filter "TestName~Add"
AOT 編譯執行
# 發佈為 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 中無法顯示或執行
解決方案:
- 確認 IDE 版本支援 Microsoft.Testing.Platform
- 啟用相關預覽功能
- 重新載入專案或重啟 IDE
問題 3:非同步斷言遺忘
症狀: 編譯錯誤或斷言無法正常執行
解決方案: 所有斷言都需要 await,測試方法必須是 async Task
適用場景評估
適合使用 TUnit
- 全新專案:沒有歷史包袱
- 效能要求高:大型測試套件(1000+ 測試)
- 技術棧先進:使用 .NET 8+,計劃採用 AOT
- CI/CD 重度使用:測試執行時間直接影響部署頻率
- 容器化部署:快速啟動時間很重要
暫時不建議
- Legacy 專案:已有大量 xUnit 測試
- 保守團隊:需要穩定性勝過創新性
- 複雜測試生態:大量使用 xUnit 特定套件
- 舊版 .NET:還在 .NET 6/7
參考資源
原始文章
本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
- Day 28 - TUnit 入門 - 下世代 .NET 測試框架探索
官方資源
Microsoft 官方文件
Weekly Installs
6
Repository
kevintsengtw/dotnet-testing-agent-skillsInstalled on
claude-code5
antigravity4
gemini-cli4
windsurf3
opencode3
codex3