Agent Skills: AutoFixture + NSubstitute 自動模擬整合

|

UncategorizedID: kevintsengtw/dotnet-testing-agent-skills/dotnet-testing-autofixture-nsubstitute-integration

Install this agent skill to your local

pnpm dlx add-skill https://github.com/kevintsengtw/dotnet-testing-agent-skills/tree/HEAD/skills/dotnet-testing-autofixture-nsubstitute-integration

Skill Files

Browse the full folder contents for dotnet-testing-autofixture-nsubstitute-integration.

Download Skill

Loading file tree…

skills/dotnet-testing-autofixture-nsubstitute-integration/SKILL.md

Skill Metadata

Name
dotnet-testing-autofixture-nsubstitute-integration
Description
|

AutoFixture + NSubstitute 自動模擬整合

核心價值

  • 減少樣板程式碼:不需要手動為每個介面建立 Substitute.For<T>()
  • 自動處理複雜相依圖:AutoFixture 會自動解析並建立所需的物件
  • 提升測試維護性:當建構函式變更時,測試程式碼通常不需要同步修改
  • 保持測試重點:讓開發者專注於測試邏輯而非物件建立

套件安裝與設定

必要套件

# 核心套件
dotnet add package AutoFixture.AutoNSubstitute

# 相關套件(如尚未安裝)
dotnet add package AutoFixture
dotnet add package AutoFixture.Xunit2
dotnet add package NSubstitute
dotnet add package xunit

NuGet 套件資訊

| 套件名稱 | 用途 | NuGet 連結 | | ----------------------------- | ------------------------------- | ------------------------------------------------------------------------ | | AutoFixture.AutoNSubstitute | AutoFixture 與 NSubstitute 整合 | nuget.org | | AutoFixture.Xunit2 | xUnit 整合(AutoData 屬性) | nuget.org | | NSubstitute | 模擬框架 | nuget.org |


核心概念

AutoNSubstituteCustomization 的作用

當在 AutoFixture 中加入 AutoNSubstituteCustomization 時,它會自動:

  1. 偵測介面類型:當 AutoFixture 遇到介面或抽象類別時
  2. 自動建立替身:使用 NSubstitute 的 Substitute.For<T>() 建立 Mock 物件
  3. 注入相依性:將這些替身物件注入到需要的建構函式中
  4. 保持實例一致性:確保相同類型的替身在同一個測試中保持一致
using AutoFixture;
using AutoFixture.AutoNSubstitute;

// 建立包含 AutoNSubstitute 功能的 Fixture
var fixture = new Fixture().Customize(new AutoNSubstituteCustomization());

// 自動建立服務和其相依性
// MyService 的所有介面相依性都會自動變成 NSubstitute 的替身
var service = fixture.Create<MyService>();

FrozenAttribute 凍結機制

[Frozen] 屬性用來控制測試中某個類型的實例:

  • 當參數被標註為 [Frozen] 時,AutoFixture 會建立這個類別的一個實例並凍結
  • 後續在測試方法中都會使用同一個已凍結的實例
  • 這對於需要設定相依性行為然後驗證 SUT 的測試特別重要
[Theory]
[AutoData]
public async Task TestMethod(
    [Frozen] IRepository repository,  // 這個 repository 會被凍結
    MyService sut)                    // sut 會使用同一個 repository
{
    // 設定凍結實例的行為
    repository.GetAsync(Arg.Any<int>()).Returns(someData);

    // SUT 內部使用的是同一個 repository 實例
    var result = await sut.DoSomething();
}

參數順序的重要性

使用 [Frozen] 時,參數順序非常重要

// 正確:Frozen 參數在 SUT 之前
public async Task TestMethod(
    [Frozen] IRepository repository,
    MyService sut)

// 錯誤:SUT 會使用不同的 repository 實例
public async Task TestMethod(
    MyService sut,
    [Frozen] IRepository repository)  // 太晚凍結了

傳統方式 vs AutoNSubstitute 方式

傳統手動方式

[Fact]
public async Task TraditionalWay()
{
    // Arrange - 手動建立每個相依性
    var repository = Substitute.For<IRepository>();
    var logger = Substitute.For<ILogger<OrderService>>();
    var notificationService = Substitute.For<INotificationService>();
    var cacheService = Substitute.For<ICacheService>();

    var sut = new OrderService(repository, logger, notificationService, cacheService);

    // 設定替身行為
    repository.GetOrderAsync(Arg.Any<int>()).Returns(someOrder);

    // Act
    var result = await sut.GetOrderAsync(orderId);

    // Assert
    result.Should().NotBeNull();
}

問題

  • 當服務增加新相依性時,所有測試都需要修改
  • 大量重複的 Substitute.For<T>() 呼叫
  • 測試程式碼冗長,難以快速理解測試意圖

使用 AutoNSubstitute 方式

[Theory]
[AutoDataWithCustomization]
public async Task WithAutoNSubstitute(
    [Frozen] IRepository repository,
    OrderService sut)
{
    // Arrange - 相依性已自動建立,只需設定需要的行為
    repository.GetOrderAsync(Arg.Any<int>()).Returns(someOrder);

    // Act
    var result = await sut.GetOrderAsync(orderId);

    // Assert
    result.Should().NotBeNull();
}

優勢

  • 只需宣告需要互動的相依性
  • 其他相依性(logger, notificationService, cacheService)自動建立
  • 建構函式變更時,測試通常不需要修改

自訂 AutoData 屬性

為什麼需要自訂 AutoData 屬性?

在實際專案中,通常需要整合多種客製化設定:

  • AutoNSubstituteCustomization:自動為介面建立 NSubstitute 替身
  • 專案特定的 Customization:如 Mapper 設定、驗證器設定等
  • 一致的測試基礎設施:確保整個專案使用相同的設定

AutoDataWithCustomizationAttribute 實作

using AutoFixture;
using AutoFixture.AutoNSubstitute;
using AutoFixture.Xunit2;

namespace MyProject.Tests.AutoFixtureConfigurations;

/// <summary>
/// 包含客製化設定的 AutoData 屬性
/// </summary>
public class AutoDataWithCustomizationAttribute : AutoDataAttribute
{
    /// <summary>
    /// 建構函式
    /// </summary>
    public AutoDataWithCustomizationAttribute() : base(CreateFixture)
    {
    }

    private static IFixture CreateFixture()
    {
        var fixture = new Fixture()
            .Customize(new AutoNSubstituteCustomization())
            .Customize(new MapsterMapperCustomization())  // 專案特定設定
            .Customize(new DomainCustomization());        // 領域模型設定

        return fixture;
    }
}

InlineAutoDataWithCustomizationAttribute 實作

用於結合固定測試值與自動產生物件:

using AutoFixture;
using AutoFixture.AutoNSubstitute;
using AutoFixture.Xunit2;

namespace MyProject.Tests.AutoFixtureConfigurations;

/// <summary>
/// 包含客製化設定的 InlineAutoData 屬性
/// </summary>
public class InlineAutoDataWithCustomizationAttribute : InlineAutoDataAttribute
{
    /// <summary>
    /// 建構函式
    /// </summary>
    /// <param name="values">固定值(將填入測試方法的前幾個參數)</param>
    public InlineAutoDataWithCustomizationAttribute(params object[] values)
        : base(new AutoDataWithCustomizationAttribute(), values)
    {
    }
}

重要實作細節

為什麼使用 new AutoDataWithCustomizationAttribute() 而不是 CreateFixture 方法?

// 錯誤:InlineAutoDataAttribute 需要 AutoDataAttribute,不是 Func<IFixture>
public InlineAutoDataWithCustomizationAttribute(params object[] values)
    : base(CreateFixture, values)  // 編譯錯誤或行為異常

// 正確:傳遞 AutoDataAttribute 實例
public InlineAutoDataWithCustomizationAttribute(params object[] values)
    : base(new AutoDataWithCustomizationAttribute(), values)

原因:

  • InlineAutoDataAttribute 繼承自 CompositeDataAttribute
  • 它需要接收一個 AutoDataAttribute 實例作為資料來源提供者
  • 這樣可以重用 AutoDataWithCustomizationAttribute 的所有設定

常見相依性的客製化處理

某些相依性(如 IMapper)不適合使用 Mock,而應該使用真實實例。包含 Mapster 和 AutoMapper 的客製化範例。

完整客製化處理範例請參考 references/dependency-customization.md


測試實作範例

涵蓋基本測試、Frozen 相依行為設定、自動產生測試資料、InlineAutoData 參數化測試、CollectionSize 控制、IFixture 複雜資料設定、Nullable 參考類型處理等完整範例。

完整測試實作範例請參考 references/test-implementation-examples.md


適用場景判斷

建議使用的場景

| 場景 | 原因 | | ---------------- | ---------------------------------- | | 服務層測試 | 通常有多個相依性,自動模擬效益最大 | | 複雜相依圖 | AutoFixture 自動處理多層相依性 | | 參數化測試 | 結合固定值與自動產生資料 | | 需要大量測試資料 | 減少手動建立測試資料的工作 | | 快速迭代開發 | 建構函式變更時測試通常不需修改 |

謹慎使用的場景

| 場景 | 原因 | | ---------------------- | ---------------------------------------- | | 單一相依性測試 | 手動建立可能更清晰直覺 | | 精確控制屬性值 | 需要額外的 fixture.Build().With() 設定 | | 團隊不熟悉 AutoFixture | 學習成本可能影響開發效率 | | 除錯困難的場景 | 自動產生的物件可能讓除錯變複雜 | | 效能敏感的測試 | 物件建立的開銷可能影響執行速度 |


最佳實踐

導入策略

  1. 漸進式採用

    • 從簡單的服務類別開始
    • 逐步擴展到複雜場景
    • 讓團隊逐漸熟悉模式
  2. 團隊培訓

    • 確保團隊理解 Frozen 機制
    • 說明參數順序的重要性
    • 分享除錯技巧
  3. 建立規範

    • 何時使用自動產生 vs 手動建立
    • 自訂 Customization 的命名與組織
    • 測試資料的控制策略

程式碼組織

MyProject.Tests/
├── AutoFixtureConfigurations/
│   ├── AutoDataWithCustomizationAttribute.cs
│   ├── InlineAutoDataWithCustomizationAttribute.cs
│   ├── AutoMapperCustomization.cs
│   └── DomainCustomization.cs
├── Services/
│   ├── OrderServiceTests.cs
│   └── ShipperServiceTests.cs
└── ...

命名慣例

  • 自訂 AutoData 屬性[專案名稱]AutoDataAttributeAutoDataWithCustomizationAttribute
  • Customization 類別[功能]Customization(如 MapsterMapperCustomization
  • 測試方法:維持 方法_情境_預期 的命名模式

注意事項與限制

常見陷阱

  1. 參數順序錯誤

    // Frozen 參數在 SUT 之後,不會生效
    public void Test(MyService sut, [Frozen] IRepository repo)
    
    // Frozen 參數必須在 SUT 之前
    public void Test([Frozen] IRepository repo, MyService sut)
    
  2. 遺忘 AutoNSubstituteCustomization

    // 沒有 AutoNSubstitute,介面會產生異常
    var fixture = new Fixture();
    
    // 加入 AutoNSubstituteCustomization
    var fixture = new Fixture().Customize(new AutoNSubstituteCustomization());
    
  3. 過度依賴自動產生

    // 測試意圖不明確
    public void Test(Order order, Customer customer, MyService sut)
    {
        var result = sut.Process(order);
        result.Should().NotBeNull();  // 驗證什麼?
    }
    
    // 明確控制關鍵屬性
    public void Test(IFixture fixture, MyService sut)
    {
        var order = fixture.Build<Order>()
                           .With(o => o.Status, OrderStatus.Pending)
                           .Create();
    
        var result = sut.Process(order);
        result.Status.Should().Be(OrderStatus.Processed);
    }
    

效能考量

  • 每個測試方法都會建立新的 Fixture 和所有相依性
  • 複雜物件圖可能增加測試執行時間
  • 考慮使用 [ClassData]IClassFixture<T> 共享設定

相關技能

| 技能名稱 | 關聯說明 | | ---------------------------- | -------------------------------------- | | autofixture-basics | AutoFixture 基礎使用,本技能的前置知識 | | autofixture-customization | 自訂 Customization 的進階用法 | | autodata-xunit-integration | AutoData 屬性家族的完整說明 | | nsubstitute-mocking | NSubstitute 基礎,Mock 設定的詳細說明 |


輸出格式

  • 產生自訂 AutoDataAttribute 衍生類別檔案(*AutoDataAttribute.cs
  • 產生 ICustomization 實作類別檔案(*Customization.cs
  • 測試方法使用 [Theory] 搭配自訂 AutoData 屬性
  • [Frozen] 參數置於 SUT 參數之前
  • 搭配 NSubstitute 的 Returns()Received() 進行行為設定與驗證

參考資源

原始文章

本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:

  • Day 13 - AutoFixture 整合 NSubstitute:自動建立 Mock 對象
    • 鐵人賽文章:https://ithelp.ithome.com.tw/articles/10375419
    • 範例程式碼:https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day13

官方文件

延伸閱讀

範例程式碼