xiaohongshu
xiaohongshu
dotnet-testing-advanced-tunit-fundamentals
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,...
Full skill instructions
本技能涵蓋 TUnit 新世代 .NET 測試框架的入門基礎,從框架特色到實際專案建立與測試撰寫。
核心主題:
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();
}
}
優勢:
JIT vs AOT 編譯流程:
傳統 JIT:C# 原始碼 → IL 中間碼 → 執行時期 JIT 編譯 → 機器碼 → 執行
AOT: C# 原始碼 → 編譯時期直接產生 → 機器碼 → 直接執行
AOT 編譯的優勢:
啟用 AOT 支援:
<PropertyGroup>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
實際效能差異:
傳統 JIT 編譯測試啟動時間:約 1-2 秒
TUnit AOT 編譯測試啟動時間:約 50-100 毫秒
(大型專案可達 10-30 倍啟動時間改善)
TUnit 建構在微軟最新的 Microsoft.Testing.Platform 之上,而非傳統的 VSTest 平台:
重要注意事項:
TUnit 專案不需要也不應該安裝 Microsoft.NET.Test.Sdk 套件。
TUnit 將並行執行設為預設,並提供精細的控制:
// 預設所有測試都會並行執行
[Test]
public async Task ParallelTest1() { }
[Test]
public async Task ParallelTest2() { }
// 需要時可以控制並行行為
[Test]
[NotInParallel("DatabaseTests")]
public async Task DatabaseTest() { }
# 建立專案目錄
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 專案模板
dotnet new install TUnit.Templates
# 使用 TUnit template 建立測試專案
dotnet new tunit -n MyApp.Tests -o tests/MyApp.Tests
<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.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);
}
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);
}
// 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 採用流暢式(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 = "[email protected]";
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();
}
[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);
}
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()
{
// 清理資源
}
}
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("[email protected]");
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)
// 預設並行執行
[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 |
|---|---|---|
| 基本測試 | [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("[email protected]", true)]
[InlineData("invalid", false)]
public void IsValidEmail_各種輸入_應回傳正確驗證結果(string email, bool expected)
{
var result = _validator.IsValidEmail(email);
Assert.Equal(expected, result);
}
TUnit 轉換後:
[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);
}
主要變更:
[Theory] → [Test][InlineData] → [Arguments]async Taskawait# 建置專案
dotnet build
# 執行所有測試
dotnet test
# 詳細輸出
dotnet test --verbosity normal
# 產生覆蓋率報告
dotnet test --coverage
# 過濾特定測試
dotnet test --filter "ClassName=CalculatorTests"
dotnet test --filter "TestName~Add"
# 發佈為 AOT 編譯版本
dotnet publish -c Release -p:PublishAot=true
# 執行 AOT 編譯的測試
./bin/Release/net9.0/publish/MyApp.Tests.exe
Visual Studio 2022:
VS Code:
JetBrains Rider:
| 場景 | xUnit | TUnit | TUnit AOT | 效能提升 |
|---|---|---|---|---|
| 簡單測試執行 | 1,400ms | 1,000ms | 60ms | 23x (AOT) |
| 非同步測試 | 1,400ms | 930ms | 26ms | 54x (AOT) |
| 並行測試 | 1,425ms | 999ms | 54ms | 26x (AOT) |
錯誤: 安裝了 Microsoft.NET.Test.Sdk 導致測試無法發現
解決方案: 移除 Microsoft.NET.Test.Sdk,TUnit 使用新的測試平台
症狀: 測試在 IDE 中無法顯示或執行
解決方案:
症狀: 編譯錯誤或斷言無法正常執行
解決方案: 所有斷言都需要 await,測試方法必須是 async Task
本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience