iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
Software Development

重啟挑戰:老派軟體工程師的測試修練系列 第 26

Day 26 – xUnit 升級指南:從 2.9.x 到 3.x 的轉換

  • 分享至 

  • xImage
  •  

前言

在 .NET 測試框架的發展史上,xUnit.net 一直是最受歡迎的選擇之一。自 2012 年首次發佈以來,它以簡潔的 API 設計、良好的擴展性和活躍的社群支援,贏得了廣大開發者的信賴。

然而,隨著 .NET 生態系統的快速演進,特別是 .NET 8+ 的推出和現代開發實踐的變化,舊版本的 xUnit 開始顯現出一些限制:執行效能有待提升、缺乏一些現代化的測試功能、對新版 .NET 平台的最佳化不足等。

xUnit v3 的重大里程碑

xUnit v3 的開發歷程相當漫長,可以追溯到約 2019 年開始的準備工作。根據官方 GitHub 程式碼庫的提交記錄,與 v3 相關的基礎架構調整和規劃工作早在數年前就已展開,這表明 v3 並非短期內開發完成,而是經過長期且深思熟慮的演進過程。

整個開發時程包含:

  • 準備工作階段 (約 2019-2022):基礎架構調整、設計理念確立
  • 正式開發階段 (2022 年底起):在 2022 年 12 月的 GitHub 討論中,開發者明確提及 v3 發布計畫,主要功能已大致完成
  • 公開預覽階段 (2024 年 7 月 31 日起):發布第一個公開預覽版本 0.2.0-pre.69,正式進入社群測試階段
  • xunit.v3 套件正式發布 (2024 年 12 月 16 日)xunit.v3 1.0.0 正式發佈,表示 xUnit v3 生態系統正式問世
  • 3.0.0 穩定版本發佈 (2025 年 7 月 13 日)xunit.v3 3.0.0 正式發佈,這是 v3 系列的第一個主要穩定版本
  • 最新穩定版本 (2025 年 8 月 15 日)xunit.v3 3.0.1 發佈,提供功能改進和錯誤修正

值得注意的是,xunit.v3 套件採用了與傳統 xUnit 不同的命名策略。當我們討論 xUnit v3 時,實際的 NuGet 套件名稱是 xunit.v3,而不是 xunit。這種命名方式讓開發者能夠明確區分版本,避免意外升級。

升級的必要性與挑戰

xUnit v3 帶來了重要的新功能和效能改進:獨立進程執行、動態跳過測試、測試上下文、改進的並行演算法等。但同時也引入了一些破壞性變更,包括最低運行時需求提升、測試專案架構調整、部分 API 變更等。

對於正在使用 xUnit 2.9.x 的團隊來說,這是一個重要的決策點:是否要投入時間和精力進行升級?升級過程會遇到什麼挑戰?如何確保升級後的測試仍然穩定可靠?

今天這篇文章將為你提供完整的升級指南,從破壞性變更分析、升級準備評估、逐步實作流程,到新功能的應用技巧,幫助你順利完成這次重要的技術升級。

本篇內容

今天的內容有:

  • 深入了解 xUnit v3 的重大變更:破壞性變更清單與影響評估
  • 制定完整的升級策略:從準備工作到實作步驟的完整規劃
  • 掌握新功能應用技巧:TestContext、動態跳過、Assembly Fixtures 等進階特色
  • 學習升級後的最佳實踐:確保測試穩定性和可維護性

xUnit 3.x 重大變更概述

破壞性變更清單

首先,讓我們來看看那些會讓你的專案無法編譯的重大變更:

1. 最低運行時需求提升

xUnit 3.x 對運行時環境有了更嚴格的要求:

  • .NET Framework 4.7.2 或更新版本
  • .NET 8 或更新版本

如果你的專案還在使用 .NET Framework 4.6.x 或 .NET Core 3.1,需要先升級框架版本。這個變更是為了利用新版本 .NET 的效能改進和功能特性。

2. 僅支援 SDK-style 專案

如果你還在使用傳統專案檔案格式,必須先升級到 SDK-style 專案格式。檢查方式很簡單:專案檔案開頭如果是 <Project Sdk="Microsoft.NET.Sdk">,就是 SDK-style 格式。

3. 測試專案變成可執行檔

這是一個重大的架構變更。在 xUnit 2.x 中,測試專案是程式庫 (Library),需要透過測試執行器來執行。而在 3.x 中,測試專案本身就是可執行檔 (Exe):

<PropertyGroup>
  <OutputType>Exe</OutputType>
</PropertyGroup>

這意味著你可以直接執行測試專案的 .exe 檔案來執行測試,不再需要外部執行器。

4. async void 測試不再支援

如果你有這樣的測試方法:

[Fact]
public async void 測試某個非同步功能()  // 這在 3.x 中會失敗
{
    // 測試邏輯
}

必須改成:

[Fact]
public async Task 測試某個非同步功能()  // 正確的寫法
{
    // 測試邏輯
}

5. IAsyncLifetime 現在繼承 IAsyncDisposable

這個變更會影響測試類別的清理邏輯。在 2.x 中,如果你同時實作了 IAsyncLifetimeIDisposable,兩個 Dispose 方法都會被呼叫。在 3.x 中,只會呼叫 DisposeAsync

6. [SkippableFact] 和 [SkippableTheory] 屬性移除

xUnit 3.x 移除了 [SkippableFact][SkippableTheory] 屬性。如果你的測試中使用了這些屬性:

// xUnit 2.x 的寫法(3.x 中已移除)
[SkippableFact]
public void 可跳過的測試()
{
    Skip.If(某個條件, "跳過原因");
    // 測試邏輯
}

需要改為使用動態跳過功能:

// xUnit 3.x 的替代方案
[Fact]
public void 可跳過的測試()
{
    if (某個條件)
    {
        Assert.Skip("跳過原因");
    }
    // 測試邏輯
}

套件名稱大變革

xUnit 3.x 引入了全新的套件命名策略,從 xunit.* 變更為 xunit.v3.*。這是一個重要的變革,當我們談論 xUnit v3 時,必須使用新的套件名稱:

v1~v2 套件名稱 最新版本 v3 套件名稱 最新版本
xunit 2.9.3 (2025-01-08) xunit.v3 3.0.1 (2025-08-15)
xunit.assert 2.9.3 xunit.v3.assert 3.0.1
xunit.core 2.9.3 xunit.v3.core 3.0.1
xunit.abstractions 2.0.3 移除,不再需要 -
xunit.runner.visualstudio 2.8.2 版本更新為 3.x.y 3.1.4

重要提醒:當我們說「xUnit v3」時,實際的 NuGet 套件名稱是 xunit.v3,不是 xunit。這個命名區別非常重要,因為:

  • xunit 套件涵蓋 v1~v2 版本系列,最新版本為 2.9.3 (發佈於 2025 年 1 月 8 日)
  • xunit.v3 套件才是真正的 3.x 版本系列
  • 兩者是完全不同的套件,可以並存但不應混用

這樣的命名變更有幾個好處:

  • 讓開發者有意識地選擇升級,而不是意外升級
  • 允許團隊遵循 SemVer 版本管理:由於 v3 有重大破壞性變更,按照 語意化版本控制 規範應該使用新的主版本號,但使用不同套件名稱避免了自動升級帶來的風險
  • 避免了套件升級過程中的依賴性問題
  • 提供清晰的版本識別,避免混淆

SemVer 版本管理補充說明

語意化版本控制 (Semantic Versioning, SemVer) 是一套版本命名規範,格式為 主版本號.次版本號.修訂號

  • 主版本號:當有不相容的 API 修改時遞增 (破壞性變更)
  • 次版本號:當新增向下相容的功能時遞增
  • 修訂號:當修正向下相容的問題時遞增

xUnit 3.x 包含大量破壞性變更,按照 SemVer 規範應該從 2.x 升級到 3.0.0。但直接升級可能導致專案無法編譯,因此 xUnit 團隊採用新套件名稱 xunit.v3 的策略,讓開發者能夠:

  • 明確選擇升級時機:避免意外的破壞性變更
  • 漸進式遷移:可以在同一個 solution 中並存兩個版本
  • 風險控制:升級前充分測試,確保相容性

新功能與改進項目

xUnit 3.x 不只是破壞性變更,還帶來了許多實用的新功能:

1. 動態跳過測試 (Dynamic Skip)

現在你可以在執行時決定是否跳過測試。xUnit v3 提供兩種方式:

使用 SkipUnless 屬性 (聲明式)

[Fact(SkipUnless = nameof(IsWindowsEnvironment), Skip = "此測試只在 Windows 環境執行")]
public void 只在Windows上執行的測試()
{
    // 只在 Windows 環境執行的測試邏輯
    var result = calculator.Add(5, 3);
    result.Should().Be(8);
}

public static bool IsWindowsEnvironment => 
    RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

重要提醒:使用 SkipUnlessSkipWhen 時,必須同時設定 Skip 屬性來提供跳過訊息,否則會發生執行錯誤。

使用 Assert.Skip (命令式)

[Fact]
public void 根據條件動態跳過的測試()
{
    // Arrange
    var isNewEngineEnabled = false; // 模擬新功能開關

    if (!isNewEngineEnabled)
    {
        Assert.Skip("新計算引擎功能尚未啟用,跳過此測試");
    }
    
    // Act & Assert
    var result = calculator.Add(5, 3);
    result.Should().Be(8);
}

2. 明確測試 (Explicit Tests)

有時候你有一些測試只想在特定情況下執行,比如整合測試或效能測試:

[Fact(Explicit = true)]
public void 昂貴的整合測試()
{
    // 這個測試預設不會執行,除非明確要求
}

3. 改進的測試診斷

xUnit 3.x 提供了更好的測試診斷功能,包含更詳細的錯誤資訊和測試執行追蹤:

[Fact]
public void 測試診斷範例()
{
    // Arrange
    var calculator = new Calculator.Core.Calculator();
    
    // Act
    var result = calculator.Add(5, 3);
    
    // Assert
    result.Should().Be(8);
    
    // xUnit 3.x 會自動提供更詳細的測試執行資訊
}

4. 矩陣理論資料 (Matrix Theory Data)

這是一個非常實用的功能,讓你可以輕鬆組合多組測試資料:

注意:以下是概念性展示,實際 API 可能因版本而異

public static TheoryData<int, string> TestData =>
    new MatrixTheoryData<int, string>(
        [42, 2112, 2600],           // 數字資料
        ["Hello", "World", "Test"]   // 字串資料
    );
    // 這會產生 3×3=9 個測試案例

[Theory]
[MemberData(nameof(TestData))]
public void 矩陣測試範例(int number, string text)
{
    // 每個數字和字串的組合都會執行一次
    number.Should().BePositive();
    text.Should().NotBeNullOrEmpty();
}

5. TestContext (測試上下文)

xUnit 3.x 引入了強大的測試上下文功能,讓你可以在測試執行過程中存取豐富的資訊和功能:

注意:TestContext 的具體 API 可能在不同版本中有所差異,以下是概念性的範例展示其用途:

[Fact]
public void TestContext基本用法展示()
{
    // Arrange
    var testName = nameof(TestContext基本用法展示);
    var startTime = DateTime.Now;

    // 模擬 TestContext 的鍵值儲存功能
    var testData = new Dictionary<string, object>
    {
        ["startTime"] = startTime,
        ["testMethod"] = testName
    };

    // Act
    var a = 5;
    var b = 3;
    var result = calculator.Add(a, b);

    // 記錄測試執行過程
    var endTime = DateTime.Now;
    var duration = endTime - startTime;

    // Assert
    result.Should().Be(8);
    
    // 模擬 TestContext 的診斷訊息功能
    // 在實際的 xUnit v3 中,可以使用 context.SendDiagnosticMessage()
}

TestContext 的主要用途包括:

  • 跨測試共用資料:在 InitializeAsync 中設定,在測試方法中讀取
  • 診斷資訊收集:記錄詳細的測試執行過程
  • 檔案附加:將測試產生的檔案加入報告
  • CancellationToken 支援:支援測試逾時和取消操作

6. [Test] 屬性與 API 簡化

xUnit 3.x 引入了通用的 [Test] 屬性,作為 [Fact][Theory] 的統一替代:

// 以下三種寫法在功能上相同
[Test]
public void 使用Test屬性的測試() 
{ 
    Assert.True(true); 
}

[Fact] 
public void 使用Fact屬性的測試() 
{ 
    Assert.True(true); 
}

// Theory 仍需使用 [Theory] 屬性
[Theory]
[InlineData(1, 2, 3)]
public void 使用Theory屬性的測試(int a, int b, int expected) 
{ 
    Assert.Equal(expected, a + b); 
}

Assert 類也進行了重構,移除了一些重複或較少使用的方法:

  • 保留核心斷言方法:Assert.TrueAssert.EqualAssert.NotNull
  • 簡化泛型約束和參數檢查
  • 改進錯誤訊息的可讀性

7. Culture 設定優化

xUnit 3.x 提供了更簡便的文化設定方式,支援多語系測試場景:

[Fact]
public void 使用英文文化的貨幣格式測試()
{
    // Arrange
    var originalCulture = Thread.CurrentThread.CurrentCulture;
    var testValue = 123.45m;

    try
    {
        // Act
        Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
        var result = testValue.ToString("C");

        // Assert
        result.Should().Be("$123.45");
    }
    finally
    {
        Thread.CurrentThread.CurrentCulture = originalCulture;
    }
}

[Fact] 
public void 使用繁體中文文化的日期格式測試()
{
    // Arrange
    var originalCulture = Thread.CurrentThread.CurrentCulture;
    var testDate = new DateTime(2024, 12, 31);

    try
    {
        // Act
        Thread.CurrentThread.CurrentCulture = new CultureInfo("zh-TW");
        var result = testDate.ToString("yyyy年MM月dd日");

        // Assert
        result.Should().Be("2024年12月31日");
    }
    finally
    {
        Thread.CurrentThread.CurrentCulture = originalCulture;
    }
}

[Theory]
[InlineData("en-US", "$123.45")]
[InlineData("zh-TW", "NT$123.45")]
public void 多文化貨幣格式測試(string culture, string expected)
{
    var originalCulture = Thread.CurrentThread.CurrentCulture;
    try
    {
        Thread.CurrentThread.CurrentCulture = new CultureInfo(culture);
        var result = 123.45m.ToString("C");
        result.Should().Be(expected);
    }
    finally
    {
        Thread.CurrentThread.CurrentCulture = originalCulture;
    }
}

升級準備與評估

在開始升級之前,我們需要仔細評估現有專案的狀況。

升級前檢查清單

我建議你按照以下順序進行檢查:

1. 確認目標框架版本

xUnit v3 的最低版本需求

  • .NET Framework 4.7.2 或更新版本
  • .NET 8.0 或更新版本 (推薦)

重要提醒:xUnit v3 不支援 .NET Core 3.1、.NET 5、.NET 6、.NET 7 等中間版本。如果你的專案使用這些版本,必須先升級到 .NET 8 或更新版本。

使用 IDE 檢查

  • 在 Visual Studio 中:右鍵點擊專案 → 屬性 → 目標框架
  • 在 VS Code 中:開啟 .csproj 檔案,查看 <TargetFramework> 節點

手動檢查專案檔案

開啟專案檔案 (.csproj),確認目標框架是否符合 xUnit v3 需求:

<PropertyGroup>
  <!-- O 支援的版本 -->
  <TargetFramework>net8.0</TargetFramework>
  <TargetFramework>net9.0</TargetFramework>
  <TargetFramework>net472</TargetFramework>
  
  <!-- X 不支援的版本,需要升級 -->
  <!-- <TargetFramework>net6.0</TargetFramework> -->
  <!-- <TargetFramework>net7.0</TargetFramework> -->
  <!-- <TargetFramework>netcoreapp3.1</TargetFramework> -->
</PropertyGroup>

如果發現專案使用不支援的版本,建議升級到 .NET 8 或更新版本以獲得最佳體驗。

2. 識別 async void 測試方法

使用 IDE 搜尋功能

在 IDE 中使用「尋找和取代」功能,搜尋以下模式:

  • 搜尋文字:async void
  • 啟用正規表達式,搜尋:async\s+void.*\[Fact\]|async\s+void.*\[Theory\]

找到的結果需要改為 async Task

手動檢查

查看測試檔案中是否有類似以下的模式:

[Fact]
public async void 某個測試方法()  // X 需要修正
{
    // 測試邏輯
}

應該改為:

[Fact]
public async Task 某個測試方法()  // O 正確
{
    // 測試邏輯
}

3. 檢查 IAsyncLifetime 實作

如果你的測試類別實作了 IAsyncLifetime,檢查是否同時實作了 IDisposable

// 需要檢查的模式
public class MyTestClass : IAsyncLifetime, IDisposable
{
    public async Task InitializeAsync() { /* ... */ }
    
    public async Task DisposeAsync() { /* ... */ }
    
    public void Dispose() { /* 在 3.x 中不會被呼叫 */ }
}

相依套件影響評估

升級 xUnit 不只是更換套件那麼簡單,還要考慮其他相依套件的相容性:

常見的相依套件檢查

  1. Mock 框架:NSubstitute、Moq 等是否支援 xUnit 3.x
  2. 斷言庫:FluentAssertions、Shouldly 等的相容性
  3. 測試資料產生器:AutoFixture、Bogus 等
  4. 測試報告工具:是否支援新的報告格式

IDE 支援與 Microsoft Testing Platform

升級到 xUnit 3.x 時,需要特別注意開發工具的相容性:

IDE 版本需求

  • Visual Studio 2022 17.8+ 或 Visual Studio Code 才能完整支援 xUnit 3.x 的新功能
  • Rider 2023.3+ 提供完整的 xUnit 3.x 支援

Microsoft Testing Platform 設定

xUnit 3.x 預設啟用 Microsoft Testing Platform,需要在專案檔案中確認:

<PropertyGroup>
  <EnableMicrosoftTestingPlatform>true</EnableMicrosoftTestingPlatform>
  <OutputType>Exe</OutputType>
</PropertyGroup>

如果你在較舊的 IDE 中遇到測試發現或執行問題,可以暫時停用:

<PropertyGroup>
  <EnableMicrosoftTestingPlatform>false</EnableMicrosoftTestingPlatform>
</PropertyGroup>

但建議儘快升級 IDE 以獲得最佳的開發體驗。

逐步升級實作

現在讓我們開始實際的升級過程。我建議採用漸進式的方法,一步一步來。

步驟 1:備份並建立測試分支

# 建立升級分支
git checkout -b feature/upgrade-xunit-v3
git push -u origin feature/upgrade-xunit-v3

或者:建立全新的 xUnit 3.x 專案

如果你想要從零開始體驗 xUnit 3.x,可以使用官方提供的專案範本:

# 安裝 xUnit 3.x 專案範本
dotnet new install xunit.v3.templates

# 建立新的 xUnit 3.x 測試專案
dotnet new xunit3 -n MyNewTestProject
cd MyNewTestProject

# 檢查產生的專案結構
Get-ChildItem -Recurse

新建立的專案會包含:

  • 正確的 OutputType 設定為 Exe
  • 最新的 xunit.v3 套件參考 (注意是 xunit.v3,不是 xunit)
  • 範例測試檔案展示新功能
  • 預設的 xunit.runner.json 設定檔

步驟 2:更新套件參考

首先更新專案檔案中的套件參考,注意必須使用 xunit.v3 套件名稱:

<!-- 移除舊的套件參考 -->
<!-- <PackageReference Include="xunit" Version="2.9.3" /> -->
<!-- <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" /> -->
<!-- <PackageReference Include="xunit.abstractions" Version="2.0.3" /> -->

<!-- 新增 xUnit v3 套件參考 - 注意使用 xunit.v3 -->
<PackageReference Include="xunit.v3" Version="3.0.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />

重要:確保使用 xunit.v3 而不是 xunit,這是 xUnit v3 的正確套件名稱。

步驟 3:修改專案設定

更新專案檔案,將輸出類型改為可執行檔:

<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>net9.0</TargetFramework>
  <!-- 其他設定 -->
</PropertyGroup>

步驟 4:修正程式碼

修正 async void 測試

// 修正前
[Fact]
public async void 測試非同步方法()
{
    var result = await SomeAsyncMethod();
    Assert.True(result);
}

// 修正後
[Fact]
public async Task 測試非同步方法()
{
    var result = await SomeAsyncMethod();
    Assert.True(result);
}

更新 using 陳述式

某些命名空間已經變更,可能需要更新:

// 可能需要更新的 using
using Xunit;
using Xunit.Abstractions; // 這個可能不再需要

步驟 5:編譯和測試

# 清理專案
dotnet clean

# 還原套件
dotnet restore

# 編譯專案
dotnet build

# 執行測試
dotnet test --verbosity normal

如果遇到編譯錯誤,不要慌張。記錄每個錯誤並逐一解決。

xUnit 3.x 進階特色

升級完成後,讓我們深入了解 xUnit 3.x 的進階功能,這些特色能大幅提升測試的功能性和可維護性。

Test Pipeline Startup (測試前期執行)

xUnit 3.x 引入了測試管道啟動機制,讓你可以在整個測試執行流程開始前進行全域初始化:

public class TestPipelineStartup : ITestPipelineStartup
{
    public async Task ConfigureAsync(ITestPipelineBuilder builder, 
                                     CancellationToken cancellationToken)
    {
        // 全域初始化邏輯
        Console.WriteLine("初始化測試環境...");
        
        // 設定全域服務
        builder.Services.AddSingleton<IConfiguration>(LoadConfiguration());
        builder.Services.AddSingleton<ILogger>(CreateLogger());
        
        // 註冊全域測試資源
        await InitializeDatabaseAsync();
    }
    
    private IConfiguration LoadConfiguration()
    {
        return new ConfigurationBuilder()
            .AddJsonFile("testsettings.json")
            .Build();
    }
    
    private async Task InitializeDatabaseAsync()
    {
        // 初始化測試資料庫
        using var connection = new SqlConnection(connectionString);
        await connection.OpenAsync();
        await SeedTestDataAsync(connection);
    }
}

// 在 AssemblyInfo.cs 或程式檔案中註冊
[assembly: TestPipelineStartup(typeof(TestPipelineStartup))]

與傳統的 CollectionFixtureClassFixture 相比,Test Pipeline Startup 的優勢:

  • 更早的執行時機:在任何測試執行前就完成初始化
  • 全域作用範圍:影響整個測試專案的所有測試
  • 服務容器整合:可以註冊依賴注入服務
  • 更好的效能:避免重複初始化相同資源

Assembly Fixtures (全組件層級設定)

xUnit 3.x 重新設計了 Assembly Fixtures,提供更強大的全組件層級資源管理:

public class DatabaseAssemblyFixture : IAsyncLifetime
{
    public string ConnectionString { get; private set; }
    public ITestDatabase Database { get; private set; }
    
    public async Task InitializeAsync()
    {
        // 建立測試資料庫實例
        Database = await TestDatabaseFactory.CreateAsync();
        ConnectionString = Database.ConnectionString;
        
        // 執行資料庫遷移
        await Database.MigrateAsync();
        
        // 植入測試資料
        await SeedTestDataAsync();
        
        Console.WriteLine($"Assembly fixture 初始化完成: {ConnectionString}");
    }
    
    public async Task DisposeAsync()
    {
        if (Database != null)
        {
            await Database.DropAsync();
            await Database.DisposeAsync();
        }
        
        Console.WriteLine("Assembly fixture 清理完成");
    }
    
    private async Task SeedTestDataAsync()
    {
        // 植入測試需要的基礎資料
        using var context = new TestDbContext(ConnectionString);
        context.Users.Add(new User { Name = "測試使用者", Email = "test@example.com" });
        await context.SaveChangesAsync();
    }
}

// 註冊 Assembly Fixture
[assembly: AssemblyFixture(typeof(DatabaseAssemblyFixture))]

// 在測試中使用
public class UserServiceTests
{
    private readonly DatabaseAssemblyFixture _dbFixture;
    
    public UserServiceTests(DatabaseAssemblyFixture dbFixture)
    {
        _dbFixture = dbFixture;
    }
    
    [Fact]
    public async Task GetUser_ExistingUser_ReturnsUser()
    {
        // Arrange
        using var context = new TestDbContext(_dbFixture.ConnectionString);
        var userService = new UserService(context);
        
        // Act
        var user = await userService.GetUserByEmailAsync("test@example.com");
        
        // Assert
        user.Should().NotBeNull();
        user.Name.Should().Be("測試使用者");
    }
}

Assembly Fixtures 適用場景:

  • 資料庫測試:建立和清理測試資料庫
  • 外部服務模擬:啟動和關閉 Docker 容器
  • 檔案系統準備:建立測試檔案目錄結構
  • 快取系統:初始化 Redis 或記憶體快取

擴充點與自訂測試發現器

xUnit 3.x 提供了更靈活的擴充機制,透過 ITest 介面統一測試的呈現方式:

// 自訂測試屬性
public class BenchmarkAttribute : Attribute, ITestMethodAttribute
{
    public string Category { get; set; }
    public int Iterations { get; set; } = 1;
    
    public BenchmarkAttribute(string category = "Performance")
    {
        Category = category;
    }
}

// 自訂測試發現器
public class BenchmarkTestDiscoverer : ITestFrameworkDiscoverer
{
    public async Task<IReadOnlyCollection<ITest>> DiscoverAsync(
        IAssemblyInfo assembly,
        ITestFrameworkDiscoveryOptions options,
        IMessageSink diagnosticMessageSink)
    {
        var tests = new List<ITest>();
        
        foreach (var type in assembly.GetTypes(false))
        {
            foreach (var method in type.GetMethods(false))
            {
                var benchmarkAttr = method.GetCustomAttributes(typeof(BenchmarkAttribute))
                                          .FirstOrDefault();
                
                if (benchmarkAttr != null)
                {
                    var benchmark = (BenchmarkAttribute)benchmarkAttr;
                    
                    // 根據 Iterations 建立多個測試實例
                    for (int i = 0; i < benchmark.Iterations; i++)
                    {
                        tests.Add(new BenchmarkTest(
                            $"{method.Name}_Iteration_{i + 1}",
                            type,
                            method,
                            benchmark.Category));
                    }
                }
            }
        }
        
        return tests;
    }
}

// 使用自訂屬性
public class PerformanceTests
{
    [Benchmark("CPU", Iterations = 5)]
    public void CPU密集型運算測試()
    {
        // 執行 CPU 密集型運算
        var result = ComplexCalculation();
        result.Should().BeGreaterThan(0);
    }
    
    [Benchmark("Memory", Iterations = 3)]
    public void 記憶體分配測試()
    {
        // 測試記憶體分配效能
        var largeArray = new int[1_000_000];
        largeArray.Length.Should().Be(1_000_000);
    }
}

自訂發現器的應用場景:

  • 效能測試:建立專門的基準測試執行邏輯
  • 整合測試:根據環境條件動態產生測試
  • 資料驅動測試:從外部資源 (API、檔案) 載入測試案例
  • 測試分類:根據特定規則組織和執行測試

新功能應用與最佳實踐

升級完成後,讓我們來看看如何充分利用 xUnit 3.x 的新功能。

改進的並行執行

xUnit 3.x 在並行執行方面有了顯著改進。你可以在 xunit.runner.json 中設定:

{
  "$schema": "https://xunit.net/schema/v3/xunit.runner.schema.json",
  "parallelAlgorithm": "conservative",
  "maxParallelThreads": 4,
  "diagnosticMessages": true
}

改善的測試診斷和報告

xUnit 3.x 提供了更好的測試診斷功能和詳細的測試報告:

public class 進階測試範例
{
    [Fact]
    public void 詳細的測試範例()
    {
        // Arrange
        var calculator = new Calculator.Core.Calculator();
        
        // Act
        var result = calculator.Add(5, 3);
        
        // Assert
        result.Should().Be(8);
        
        // xUnit 3.x 會自動提供更詳細的測試執行資訊
    }
    
    [Theory]
    [InlineData(1, 2, 3)]
    [InlineData(-1, 1, 0)]
    [InlineData(0, 0, 0)]
    public void 參數化測試範例(int a, int b, int expected)
    {
        // Arrange
        var calculator = new Calculator.Core.Calculator();
        
        // Act
        var result = calculator.Add(a, b);
        
        // Assert
        result.Should().Be(expected);
    }
}

動態跳過測試的實際應用

public class 環境相關測試
{
    [Fact]
    public void 根據環境變數跳過的測試()
    {
        // Arrange
        var enableIntegrationTests = Environment.GetEnvironmentVariable("ENABLE_INTEGRATION_TESTS");

        if (string.IsNullOrEmpty(enableIntegrationTests) || enableIntegrationTests.ToLower() != "true")
        {
            Assert.Skip("整合測試已停用。設定 ENABLE_INTEGRATION_TESTS=true 來執行此測試");
        }

        // Act & Assert
        // 測試邏輯...
    }
    
    [Fact]
    public void 根據功能開關跳過測試()
    {
        // Arrange
        var featureFlag = GetFeatureFlag("NEW_CALCULATION_ENGINE");
        if (!featureFlag)
        {
            Assert.Skip("新計算引擎功能尚未啟用");
        }
        
        // Act & Assert
        // 測試新功能...
    }
        
    private bool GetFeatureFlag(string flagName)
    {
        // 從設定或外部服務取得功能開關狀態
        return bool.Parse(Environment.GetEnvironmentVariable($"FEATURE_{flagName}") ?? "false");
    }
}

新的測試報告格式

xUnit 3.x 支援更多的報告格式,包括 CTRF 和 TRX:

# 產生 CTRF 格式報告
dotnet run -- -ctrf results.json

# 產生 TRX 格式報告  
dotnet run -- -trx results.trx

# 產生多種格式報告
dotnet run -- -xml results.xml -ctrf results.json -trx results.trx

常見問題與解決方案

在升級過程中,你可能會遇到一些常見問題:

問題 1:編譯錯誤 - 找不到 xunit.abstractions

錯誤訊息The type or namespace name 'Abstractions' does not exist in the namespace 'Xunit'

解決方案:移除對 xunit.abstractions 的參考,相關類型已移動到其他命名空間:

// 舊的寫法
using Xunit.Abstractions;
ITestOutputHelper output;

// 新的寫法
using Xunit;
ITestOutputHelper output; // 現在在 Xunit 命名空間中

問題 2:async void 測試執行失敗

錯誤訊息:測試在執行時立即失敗

解決方案:將所有 async void 測試方法改為 async Taskasync ValueTask

問題 3:自訂屬性無法運作

如果你有自訂的測試屬性,可能需要更新實作方式:

// xUnit 2.x 的實作方式
public class CustomDataAttribute : DataAttribute
{
    public override IEnumerable<object[]> GetData(MethodInfo testMethod)
    {
        // 舊的實作
    }
}

// xUnit 3.x 的實作方式
public class CustomDataAttribute : DataAttribute
{
    public override async Task<IReadOnlyCollection<ITheoryDataRow>> GetDataAsync(
        MethodInfo method, 
        DisposalTracker disposalTracker)
    {
        // 新的實作,支援非同步和 disposal tracking
        var data = await GenerateDataAsync();
        return data.Select(item => new TheoryDataRow(item)).ToList();
    }
}

升級檢查清單

為了確保升級過程順利,我整理了一份完整的檢查清單:

升級前檢查

  • [ ] 確認目標框架版本 (.NET Framework 4.7.2+ 或 .NET 8+)
  • [ ] 檢查專案檔案格式 (SDK-style)
  • [ ] 識別所有 async void 測試方法
  • [ ] 檢查 IAsyncLifetime 實作
  • [ ] 評估相依套件相容性
  • [ ] 記錄現有套件版本
  • [ ] 建立備份分支

升級過程檢查

  • [ ] 更新所有 xUnit 套件參考 (使用 xunit.v3 而非 xunit)
  • [ ] 移除 xunit.abstractions 參考
  • [ ] 修改專案檔案 OutputType 為 Exe
  • [ ] 修正所有 async void 測試方法
  • [ ] 更新 using 陳述式
  • [ ] 重構自訂屬性 (如有)
  • [ ] 驗證編譯成功
  • [ ] 執行所有測試

升級後驗證

  • [ ] 功能完整性測試
  • [ ] 效能基準比較
  • [ ] CI/CD Pipeline 驗證
  • [ ] 文檔更新
  • [ ] 團隊培訓和知識轉移

CI/CD Pipeline 驗證詳細說明

升級到 xUnit v3 後,CI/CD Pipeline 可能需要調整以適應新的架構變更:

1. 測試執行方式驗證
# 確認測試仍能在 CI 環境中正常執行
dotnet test --configuration Release --logger trx --results-directory TestResults
2. 測試報告格式檢查

xUnit v3 支援新的報告格式,確認 CI 工具能正確解析:

# 生成多種格式的測試報告
dotnet run --project TestProject -- --xml results.xml --trx results.trx --ctrf results.json
3. Microsoft Testing Platform 相容性

如果 CI 環境使用舊版工具,可能需要調整設定:

<!-- 在專案檔案中,如果遇到相容性問題可暫時停用 -->
<PropertyGroup>
  <EnableMicrosoftTestingPlatform>false</EnableMicrosoftTestingPlatform>
</PropertyGroup>
4. 建置代理程式支援

確認 CI 建置代理程式支援 xUnit v3 需求:

  • 安裝 .NET 8+ SDK
  • 更新 Visual Studio Build Tools (如適用)
  • 驗證測試探索和執行功能
5. 測試結果收集

驗證測試結果能正確收集到 CI 系統中:

  • Azure DevOps:確認測試結果能正確顯示在 Test Results 頁面
  • GitHub Actions:驗證測試摘要和失敗報告
  • Jenkins:檢查測試報告外掛程式相容性
6. 並行執行設定

xUnit v3 的並行執行可能需要調整 CI 環境設定:

// xunit.runner.json - 針對 CI 環境優化
{
  "parallelAlgorithm": "conservative",
  "maxParallelThreads": 2,  // 根據 CI 環境資源調整
  "diagnosticMessages": false
}

效能比較與優化

升級到 xUnit 3.x 後,你應該會注意到一些效能改進:

測試啟動時間

由於測試專案現在是獨立的可執行檔,啟動時間通常會更快:

# 比較測試執行時間
Measure-Command { dotnet test --verbosity quiet }

並行執行改進

xUnit 3.x 的新並行演算法提供了更好的負載平衡:

{
  "parallelAlgorithm": "conservative",  // 預設值,更穩定
  "maxParallelThreads": -1              // 使用所有可用核心
}

記憶體使用優化

獨立進程執行提供了更好的記憶體隔離,減少測試之間的干擾。

結語

升級到 xUnit v3 (使用 xunit.v3 套件) 確實需要處理一些破壞性變更,但帶來的改進絕對值得投入這些時間:

技術提升

  • 效能大幅改進:獨立進程執行和更聰明的並行演算法
  • 功能更豐富:動態跳過、測試上下文、矩陣資料等實用特色
  • 診斷能力增強:詳細的測試報告和診斷資訊,讓 debug 更輕鬆

開發體驗

  • 未來保證:基於最新 .NET 平台,確保長期支援
  • 工具整合:與 Visual Studio 和 CI/CD 管道更好的整合
  • 測試可維護性:新的 API 設計讓測試程式碼更清晰易懂

重要提醒:記住 xUnit v3 使用的是 xunit.v3 套件名稱,不是 xunit。這個命名區別很重要,確保在升級過程中使用正確的套件參考。

我的建議是採用分階段升級策略:先在小專案試水溫,確認流程沒問題後再處理大型專案。每個步驟都要仔細測試驗證,特別是那些使用自訂屬性或複雜測試邏輯的部分。

升級完成後,記得花點時間熟悉新功能,特別是 TestContext 和動態跳過這些實用特色,它們能讓你的測試更智慧、更好維護。

延伸學習

本文主要介紹了 xUnit v3 的核心升級流程和常用新功能,但 xUnit v3 還包含許多進階特色未在此文中詳述,例如:

  • Test Pipeline Startup 機制
  • 自訂測試發現器與 ITest 介面
  • Microsoft Testing Platform 深度整合
  • 更多測試報告格式選項
  • 進階測試診斷功能

如果你想深入了解所有 xUnit v3 新功能,建議參考 xUnit v3 官方新功能文件,裡面有完整的功能清單和詳細說明。

相關資源

官方文件

NuGet 套件

  • xunit.v3 套件 - xUnit v3 主要套件 (最新版本:3.0.1,發佈於 2025-08-15)
  • xunit 套件 - xUnit v1~v2 主要套件 (最新版本:2.9.3,發佈於 2025-01-08)

YouTube 影片

範例程式碼


這是「重啟挑戰:老派軟體工程師的測試修練」的第二十六天。明天會介紹 Day 27 – GitHub Copilot 測試實戰:AI 輔助測試開發指南。


上一篇
Day 25 – .NET Aspire 整合測試實戰:從 Testcontainers 到 .NET Aspire Testing
下一篇
Day 27 – GitHub Copilot 測試實戰:AI 輔助測試開發指南
系列文
重啟挑戰:老派軟體工程師的測試修練27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言