home / skills / kevintsengtw / dotnet-testing-agent-skills / dotnet-testing-autofixture-nsubstitute-integration

dotnet-testing-autofixture-nsubstitute-integration skill

/skills/dotnet-testing-autofixture-nsubstitute-integration

This skill streamlines .NET testing by auto-mocking interface dependencies with AutoFixture AutoNSubstitute, reducing boilerplate and improving test resilience.

npx playbooks add skill kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-autofixture-nsubstitute-integration

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

Files (6)
SKILL.md
15.0 KB
---
name: dotnet-testing-autofixture-nsubstitute-integration
description: |
  AutoFixture 與 NSubstitute 整合指南 - 實現自動模擬 (Auto-Mocking)。當需要自動建立 Mock 物件、簡化複雜相依性注入測試時使用。涵蓋 AutoNSubstituteDataAttribute、Frozen 機制、Greedy 建構策略。包含 IMapper (AutoMapper/Mapster) 等特殊相依性的客製化處理。
  Keywords: autofixture nsubstitute, auto mocking, AutoNSubstituteDataAttribute, 自動模擬, Frozen, AutoNSubstituteCustomization, AutoFixture.AutoNSubstitute, Greedy, fixture.Freeze, Received(), Returns(), IMapper, AutoMapper, Mapster, mapper testing
license: MIT
metadata:
  author: Kevin Tseng
  version: "1.0.0"
  tags: "autofixture, nsubstitute, auto-mocking, dependency-injection, xunit, testing"
  related_skills: "nsubstitute-mocking, autofixture-basics, autodata-xunit-integration"
---

# AutoFixture + NSubstitute 自動模擬整合

## 適用情境

本技能介紹如何整合 AutoFixture 與 NSubstitute,透過 `AutoFixture.AutoNSubstitute` 套件實現自動模擬(Auto-Mocking)功能。這種整合方式可以大幅簡化具有多個相依性的服務類別測試,讓開發者專注於測試邏輯本身,而非繁瑣的物件建立過程。

### 適用情境

當被要求執行以下任務時,請使用此技能:

- 測試具有多個介面相依性的服務類別
- 建立自動模擬所有介面相依性的測試設定
- 使用 `[Frozen]` 屬性確保相依性實例在測試中保持一致
- 建立專案級的自訂 AutoData 屬性來整合多種客製化設定
- 結合固定測試值與自動產生物件的參數化測試

### 核心價值

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

---

## 套件安裝與設定

### 必要套件

```bash
# 核心套件
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](https://www.nuget.org/packages/AutoFixture.AutoNSubstitute/) |
| `AutoFixture.Xunit2`          | xUnit 整合(AutoData 屬性)     | [nuget.org](https://www.nuget.org/packages/AutoFixture.Xunit2/)          |
| `NSubstitute`                 | 模擬框架                        | [nuget.org](https://www.nuget.org/packages/NSubstitute/)                 |

---

## 核心概念

### AutoNSubstituteCustomization 的作用

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

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

```csharp
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 的測試特別重要

```csharp
[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]` 時,**參數順序非常重要**:

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

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

---

## 傳統方式 vs AutoNSubstitute 方式

### 傳統手動方式

```csharp
[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 方式

```csharp
[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 實作

```csharp
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 實作

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

```csharp
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` 方法?**

```csharp
// ❌ 錯誤: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](references/dependency-customization.md)

---

## 測試實作範例

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

> 完整測試實作範例請參考 [references/test-implementation-examples.md](references/test-implementation-examples.md)

---

## 適用場景判斷

### 建議使用的場景

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

### 謹慎使用的場景

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

---

## 最佳實踐

### 導入策略

1. **漸進式採用**
   - 從簡單的服務類別開始
   - 逐步擴展到複雜場景
   - 讓團隊逐漸熟悉模式

2. **團隊培訓**
   - 確保團隊理解 Frozen 機制
   - 說明參數順序的重要性
   - 分享除錯技巧

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

### 程式碼組織

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

### 命名慣例

- **自訂 AutoData 屬性**:`[專案名稱]AutoDataAttribute` 或 `AutoDataWithCustomizationAttribute`
- **Customization 類別**:`[功能]Customization`(如 `MapsterMapperCustomization`)
- **測試方法**:維持 `方法_情境_預期` 的命名模式

---

## 注意事項與限制

### 常見陷阱

1. **參數順序錯誤**

   ```csharp
   // ❌ Frozen 參數在 SUT 之後,不會生效
   public void Test(MyService sut, [Frozen] IRepository repo)
   
   // ✅ Frozen 參數必須在 SUT 之前
   public void Test([Frozen] IRepository repo, MyService sut)
   ```

2. **遺忘 AutoNSubstituteCustomization**

   ```csharp
   // ❌ 沒有 AutoNSubstitute,介面會產生異常
   var fixture = new Fixture();
   
   // ✅ 加入 AutoNSubstituteCustomization
   var fixture = new Fixture().Customize(new AutoNSubstituteCustomization());
   ```

3. **過度依賴自動產生**

   ```csharp
   // ❌ 測試意圖不明確
   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 設定的詳細說明  |

---

## 參考資源

### 原始文章

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

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

### 官方文件

- [AutoFixture.AutoNSubstitute NuGet Package](https://www.nuget.org/packages/AutoFixture.AutoNSubstitute/)
- [AutoFixture Documentation - Auto Mocking](https://autofixture.readthedocs.io/en/stable/)
- [NSubstitute Documentation](https://nsubstitute.github.io/help/getting-started/)

### 延伸閱讀

- [使用 AutoFixture.AutoData 來改寫以前的測試程式碼 | mrkt的程式學習筆記](https://www.dotblogs.com.tw/mrkt/2024/09/29/191300)


### 範例程式碼

- [custom-autodata-attributes.cs](templates/custom-autodata-attributes.cs) - 自訂 AutoData 屬性範本
- [frozen-patterns.cs](templates/frozen-patterns.cs) - Frozen 機制使用模式
- [service-testing-examples.cs](templates/service-testing-examples.cs) - 服務層測試完整範例

Overview

This skill explains how to integrate AutoFixture with NSubstitute to achieve auto-mocking for .NET unit tests. It focuses on practical patterns: AutoNSubstituteCustomization, Frozen behavior, Greedy constructor strategy, and handling special dependencies like IMapper (AutoMapper/Mapster). The goal is to reduce boilerplate and keep tests focused on behavior, not object wiring.

How this skill works

The integration wires AutoFixture to create NSubstitute substitutes automatically for interfaces and abstract types using AutoNSubstituteCustomization. Marking parameters with [Frozen] ensures a single shared instance is reused for setup and verification. Custom AutoData attributes encapsulate project-specific customizations (mappers, domain builders) and InlineAutoData variants combine fixed values with auto-generated objects.

When to use it

  • Testing service classes with multiple interface dependencies
  • When you want automatic creation of mocks to reduce Substitute.For<T>() calls
  • When you need a frozen dependency instance to configure behavior and verify interactions
  • When you need consistent, reusable fixture configuration across the test suite
  • When combining fixed test values with auto-generated complex objects

Best practices

  • Introduce AutoFixture+NSubstitute gradually: start with simple services before expanding
  • Always place [Frozen] parameters before the SUT in test method signatures
  • Encapsulate project customizations (IMapper, domain factories) into a single AutoData attribute
  • Prefer real mapper instances for IMapper-like dependencies via a customization rather than mocking
  • Use fixture.Build().With(...) to control critical property values and keep intent explicit

Example use cases

  • Verify a service method calls a repository and returns transformed result with [Frozen] repository and auto-created SUT
  • Create InlineAutoData tests where the first parameters are explicit values and remaining dependencies are auto-mocked
  • Define a project-level AutoDataWithCustomization to register Mapster/AutoMapper and AutoNSubstitute for consistent tests
  • Test a complex object graph where AutoFixture resolves nested dependencies automatically
  • Freeze a collaborator to set Returns() and assert Received() was invoked on it

FAQ

Why must [Frozen] appear before the SUT parameter?

AutoFixture resolves parameters in order. If [Frozen] appears after the SUT, the SUT gets a different instance. Placing [Frozen] first ensures the SUT receives the frozen instance.

When should I use a real mapper instead of a mock?

Use a real mapper (AutoMapper/Mapster) when mapping logic is simple or critical to the behavior under test. Register it via a customization to avoid fragile mock setups.

How do I combine fixed values with auto-generated dependencies?

Use an InlineAutoData variant that wraps your AutoDataWithCustomization. Pass fixed values first; the attribute injects auto-generated dependencies for the remaining parameters.