home / skills / kevintsengtw / dotnet-testing-agent-skills / dotnet-testing-advanced-tunit-fundamentals

dotnet-testing-advanced-tunit-fundamentals skill

/skills/dotnet-testing-advanced-tunit-fundamentals

This skill helps you adopt the TUnit testing framework for fast, AOT-friendly tests driven by source generators and fine-grained lifecycle control.

npx playbooks add skill kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-advanced-tunit-fundamentals

Review the files below or copy the command above to add this skill to your agents.

Files (9)
SKILL.md
12.9 KB
---
name: dotnet-testing-advanced-tunit-fundamentals
description: |
  TUnit 新世代測試框架入門完整指南。當需要使用 TUnit 建立測試專案或從 xUnit 遷移至 TUnit 時使用。涵蓋 Source Generator 驅動測試發現、AOT 編譯支援、流暢式非同步斷言。包含專案建立、[Test] 屬性、生命週期管理、並行控制與 xUnit 語法對照。
  Keywords: TUnit, tunit testing, source generator testing, AOT testing, 新世代測試框架, [Test], [Arguments], TUnit.Assertions, Assert.That, Before(Test), After(Test), NotInParallel, TUnit.Templates, Microsoft.Testing.Platform, TUnit vs xUnit, 並行執行
license: MIT
metadata:
  author: Kevin Tseng
  version: "1.0.0"
  tags: "tunit, testing-framework, source-generator, aot, modern-testing, performance"
  related_skills: "advanced-tunit-advanced, xunit-project-setup, unit-test-fundamentals"
---

# 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
<PropertyGroup>
    <PublishAot>true</PublishAot>
    <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
```

**實際效能差異:**

```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
<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 設定

```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)斷言設計,所有斷言都是非同步的。支援相等性、布林值、數值比較、字串、集合、例外等多種斷言,並可透過 `And` / `Or` 組合條件。

```csharp
// 基本用法示例
await Assert.That(actual).IsEqualTo(expected);
await Assert.That(email).Contains("@").And.EndsWith(".com");
await Assert.That(() => action()).Throws<InvalidOperationException>();
```

> 📖 完整斷言類型與範例請參閱 [TUnit 斷言系統詳細說明](references/tunit-assertions-detail.md)

---

## 測試生命週期管理

TUnit 支援建構式 / `Dispose` 模式,以及 `[Before(Test)]`、`[Before(Class)]`、`[After(Test)]`、`[After(Class)]` 等屬性,提供比 xUnit 更細緻的生命週期控制。

```text
執行順序:Before(Class) → 建構式 → Before(Test) → 測試方法 → After(Test) → Dispose → After(Class)
```

> 📖 完整生命週期範例與屬性對照表請參閱 [生命週期管理詳細說明](references/lifecycle-management.md)

---

## 並行執行控制

### 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<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 原始程式碼:**

```csharp
[Theory]
[InlineData("[email protected]", true)]
[InlineData("invalid", false)]
public void IsValidEmail_各種輸入_應回傳正確驗證結果(string email, bool expected)
{
    var result = _validator.IsValidEmail(email);
    Assert.Equal(expected, result);
}
```

**TUnit 轉換後:**

```csharp
[Test]
[Arguments("[email protected]", 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)

Overview

This skill teaches TUnit, the next-generation .NET testing framework, and provides a practical guide for creating new test projects or migrating from xUnit. It covers Source Generator driven test discovery, AOT support, fluent async assertions, lifecycle control, and parallel execution management. The content focuses on concrete project setup, attribute usage, and migration patterns.

How this skill works

TUnit performs test discovery at compile time using a Source Generator, eliminating runtime reflection and enabling Native AOT compatibility. All tests and assertions are async-first: test methods return Task and assertions use awaitable fluent APIs. The framework runs on Microsoft.Testing.Platform and uses attributes like [Test], [Arguments], Before/After variants, and NotInParallel to control lifecycle and concurrency.

When to use it

  • Starting a new .NET test suite with modern tooling and AOT in mind
  • Migrating small to medium xUnit suites where runtime overhead and startup time matter
  • Large test suites (1000+ tests) where startup and CI turnaround are critical
  • Containerized or CI environments that benefit from fast, predictable startup
  • When you want finer lifecycle hooks and explicit parallel control

Best practices

  • Make every test method async Task and await all assertions
  • Use TUnit.Templates to scaffold projects quickly and avoid manual misconfiguration
  • Do not install Microsoft.NET.Test.Sdk in TUnit test projects
  • Group non-parallel tests with NotInParallel to isolate shared resources like databases
  • Enable PublishAot and InvariantGlobalization for AOT runs in CI when targeting net8+/net9

Example use cases

  • Create a fast startup test runner for microservice CI pipelines using AOT
  • Convert xUnit InlineData/Theory tests to [Test] + [Arguments] and async assertions
  • Run large integration suites with fine lifecycle hooks: Before(Class), Before(Test), After(Test), After(Class)
  • Isolate flaky database tests by tagging them with NotInParallel groups
  • Generate compact test binaries for container deployments using Native AOT

FAQ

Do I need Microsoft.NET.Test.Sdk for TUnit projects?

No. TUnit uses Microsoft.Testing.Platform and installing Microsoft.NET.Test.Sdk breaks test discovery. Remove it from test project dependencies.

Why must tests be async?

TUnit’s assertion system is async-first and its Source Generator generates awaitable assertions. Test methods must return Task and await assertions to compile and run correctly.