dotnet-testing-autofixture-nsubstitute-integration
SKILL.md
AutoFixture + NSubstitute 自動模擬整合
技能概述
本技能介紹如何整合 AutoFixture 與 NSubstitute,透過 AutoFixture.AutoNSubstitute 套件實現自動模擬(Auto-Mocking)功能。這種整合方式可以大幅簡化具有多個相依性的服務類別測試,讓開發者專注於測試邏輯本身,而非繁瑣的物件建立過程。
適用情境
當被要求執行以下任務時,請使用此技能:
- 測試具有多個介面相依性的服務類別
- 建立自動模擬所有介面相依性的測試設定
- 使用
[Frozen]屬性確保相依性實例在測試中保持一致 - 建立專案級的自訂 AutoData 屬性來整合多種客製化設定
- 結合固定測試值與自動產生物件的參數化測試
核心價值
- 減少樣板程式碼:不需要手動為每個介面建立
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 時,它會自動:
- 偵測介面類型:當 AutoFixture 遇到介面或抽象類別時
- 自動建立替身:使用 NSubstitute 的
Substitute.For<T>()建立 Mock 物件 - 注入相依性:將這些替身物件注入到需要的建構函式中
- 保持實例一致性:確保相同類型的替身在同一個測試中保持一致
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 客製化(Mapster 範例)
某些相依性不適合使用 Mock,而應該使用真實實例:
using AutoFixture;
using Mapster;
using MapsterMapper;
namespace MyProject.Tests.AutoFixtureConfigurations;
/// <summary>
/// Mapster 對應器客製化
/// </summary>
public class MapsterMapperCustomization : ICustomization
{
private IMapper? _mapper;
public void Customize(IFixture fixture)
{
fixture.Register(() => this.Mapper);
}
private IMapper Mapper
{
get
{
if (this._mapper is not null)
{
return this._mapper;
}
var typeAdapterConfig = new TypeAdapterConfig();
typeAdapterConfig.Scan(typeof(ServiceMapRegister).Assembly);
this._mapper = new Mapper(typeAdapterConfig);
return this._mapper;
}
}
}
為什麼 IMapper 不用 Mock?
- 工具型相依性:Mapper 不是業務邏輯,是物件對應工具
- 驗證對應邏輯:測試需要驗證對應是否正確,Mock 會失去這個能力
- 設定複雜度:為每個對應方法設定 Returns 反而增加複雜度
- 測試意圖:我們要測試業務邏輯,不是 Mapper 的行為
AutoMapper 客製化範例
using AutoFixture;
using AutoMapper;
namespace MyProject.Tests.AutoFixtureConfigurations;
public class AutoMapperCustomization : ICustomization
{
private IMapper? _mapper;
public void Customize(IFixture fixture)
{
fixture.Register(() => this.Mapper);
}
private IMapper Mapper
{
get
{
if (this._mapper is not null)
{
return this._mapper;
}
var configuration = new MapperConfiguration(cfg =>
{
cfg.AddMaps(typeof(MappingProfile).Assembly);
});
this._mapper = configuration.CreateMapper();
return this._mapper;
}
}
}
測試實作範例
基本測試:無需設定相依行為
當測試只需要驗證 SUT 本身的邏輯(如參數驗證)時:
[Theory]
[AutoDataWithCustomization]
public async Task IsExistsAsync_輸入的ShipperId為0時_應拋出ArgumentOutOfRangeException(
ShipperService sut)
{
// Arrange
var shipperId = 0;
// Act
var exception = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
() => sut.IsExistsAsync(shipperId));
// Assert
exception.Message.Should().Contain(nameof(shipperId));
}
進階測試:設定相依行為
使用 [Frozen] 取得相依性並設定其行為:
[Theory]
[AutoDataWithCustomization]
public async Task IsExistsAsync_輸入的ShipperId_資料不存在_應回傳false(
[Frozen] IShipperRepository shipperRepository,
ShipperService sut)
{
// Arrange
var shipperId = 99;
shipperRepository.IsExistsAsync(Arg.Any<int>()).Returns(false);
// Act
var actual = await sut.IsExistsAsync(shipperId);
// Assert
actual.Should().BeFalse();
}
使用自動產生的測試資料
AutoFixture 同時產生 SUT 和測試資料:
[Theory]
[AutoDataWithCustomization]
public async Task GetAsync_輸入的ShipperId_資料有存在_應回傳model(
[Frozen] IShipperRepository shipperRepository,
ShipperService sut,
ShipperModel model) // AutoFixture 自動產生
{
// Arrange
var shipperId = model.ShipperId;
shipperRepository.IsExistsAsync(Arg.Any<int>()).Returns(true);
shipperRepository.GetAsync(Arg.Any<int>()).Returns(model);
// Act
var actual = await sut.GetAsync(shipperId);
// Assert
actual.Should().NotBeNull();
actual.ShipperId.Should().Be(shipperId);
}
參數化測試:InlineAutoData
結合固定測試值與自動產生的 SUT:
[Theory]
[InlineAutoDataWithCustomization(0, 10, nameof(from))]
[InlineAutoDataWithCustomization(-1, 10, nameof(from))]
[InlineAutoDataWithCustomization(1, 0, nameof(size))]
[InlineAutoDataWithCustomization(1, -1, nameof(size))]
public async Task GetCollectionAsync_from與size輸入不合規格內容_應拋出ArgumentOutOfRangeException(
int from,
int size,
string parameterName,
ShipperService sut) // 自動產生
{
// Act
var exception = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
() => sut.GetCollectionAsync(from, size));
// Assert
exception.Message.Should().Contain(parameterName);
}
使用 CollectionSize 控制集合大小
[Theory]
[AutoDataWithCustomization]
public async Task GetAllAsync_資料表裡有10筆資料_回傳的集合裡有10筆(
[Frozen] IShipperRepository shipperRepository,
ShipperService sut,
[CollectionSize(10)] IEnumerable<ShipperModel> models)
{
// Arrange
shipperRepository.GetAllAsync().Returns(models);
// Act
var actual = await sut.GetAllAsync();
// Assert
actual.Should().NotBeEmpty();
actual.Should().HaveCount(10);
}
複雜資料設定:使用 IFixture
當需要精確控制測試資料時:
[Theory]
[AutoDataWithCustomization]
public async Task SearchAsync_companyName輸入資料_有符合條件的資料_回傳集合應包含符合條件的資料(
IFixture fixture,
[Frozen] IShipperRepository shipperRepository,
ShipperService sut)
{
// Arrange
const string companyName = "test";
var models = fixture.Build<ShipperModel>()
.With(x => x.CompanyName, companyName)
.CreateMany(1);
shipperRepository.GetTotalCountAsync().Returns(1);
shipperRepository.SearchAsync(Arg.Any<string>(), Arg.Any<string>())
.Returns(models);
// Act
var actual = await sut.SearchAsync(companyName, string.Empty);
// Assert
actual.Should().NotBeEmpty();
actual.Should().HaveCount(1);
actual.Any(x => x.CompanyName == companyName).Should().BeTrue();
}
Nullable 參考類型處理
測試 null 或空值參數時的處理方式:
[Theory]
[InlineAutoDataWithCustomization(null!, null!)]
[InlineAutoDataWithCustomization("", "")]
[InlineAutoDataWithCustomization(" ", " ")]
public async Task SearchAsync_companyName與phone都為空白_應拋出ArgumentException(
string? companyName,
string? phone,
ShipperService sut)
{
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => sut.SearchAsync(companyName!, phone!));
exception.Message.Should().Contain("companyName 與 phone 不可都為空白");
}
處理說明:
- 參數宣告使用
string?:因為測試需要傳入null值 - InlineAutoData 中使用
null!:告訴編譯器這是刻意的測試資料 - 方法呼叫使用
!運算子:在測試中使用 null-forgiving 運算子
適用場景判斷
建議使用的場景
| 場景 | 原因 |
|---|---|
| 服務層測試 | 通常有多個相依性,自動模擬效益最大 |
| 複雜相依圖 | AutoFixture 自動處理多層相依性 |
| 參數化測試 | 結合固定值與自動產生資料 |
| 需要大量測試資料 | 減少手動建立測試資料的工作 |
| 快速迭代開發 | 建構函式變更時測試通常不需修改 |
謹慎使用的場景
| 場景 | 原因 |
|---|---|
| 單一相依性測試 | 手動建立可能更清晰直覺 |
| 精確控制屬性值 | 需要額外的 fixture.Build().With() 設定 |
| 團隊不熟悉 AutoFixture | 學習成本可能影響開發效率 |
| 除錯困難的場景 | 自動產生的物件可能讓除錯變複雜 |
| 效能敏感的測試 | 物件建立的開銷可能影響執行速度 |
最佳實踐
導入策略
-
漸進式採用
- 從簡單的服務類別開始
- 逐步擴展到複雜場景
- 讓團隊逐漸熟悉模式
-
團隊培訓
- 確保團隊理解 Frozen 機制
- 說明參數順序的重要性
- 分享除錯技巧
-
建立規範
- 何時使用自動產生 vs 手動建立
- 自訂 Customization 的命名與組織
- 測試資料的控制策略
程式碼組織
MyProject.Tests/
├── AutoFixtureConfigurations/
│ ├── AutoDataWithCustomizationAttribute.cs
│ ├── InlineAutoDataWithCustomizationAttribute.cs
│ ├── AutoMapperCustomization.cs
│ └── DomainCustomization.cs
├── Services/
│ ├── OrderServiceTests.cs
│ └── ShipperServiceTests.cs
└── ...
命名慣例
- 自訂 AutoData 屬性:
[專案名稱]AutoDataAttribute或AutoDataWithCustomizationAttribute - Customization 類別:
[功能]Customization(如MapsterMapperCustomization) - 測試方法:維持
方法_情境_預期的命名模式
注意事項與限制
常見陷阱
-
參數順序錯誤
// ❌ Frozen 參數在 SUT 之後,不會生效 public void Test(MyService sut, [Frozen] IRepository repo) // ✅ Frozen 參數必須在 SUT 之前 public void Test([Frozen] IRepository repo, MyService sut) -
遺忘 AutoNSubstituteCustomization
// ❌ 沒有 AutoNSubstitute,介面會產生異常 var fixture = new Fixture(); // ✅ 加入 AutoNSubstituteCustomization var fixture = new Fixture().Customize(new AutoNSubstituteCustomization()); -
過度依賴自動產生
// ❌ 測試意圖不明確 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 對象
官方文件
- AutoFixture.AutoNSubstitute NuGet Package
- AutoFixture Documentation - Auto Mocking
- NSubstitute Documentation
延伸閱讀
範例檔案
請參考同目錄下的範例程式碼:
- custom-autodata-attributes.cs - 自訂 AutoData 屬性範本
- frozen-patterns.cs - Frozen 機制使用模式
- service-testing-examples.cs - 服務層測試完整範例
Weekly Installs
6
Repository
kevintsengtw/dotnet-testing-agent-skillsInstalled on
claude-code5
antigravity4
gemini-cli4
windsurf3
opencode3
codex3