xiaohongshu
xiaohongshu
dotnet-testing-autofixture-nsubstitute-integration
AutoFixture 與 NSubstitute 整合指南 - 實現自動模擬 (Auto-Mocking)。涵蓋 AutoNSubstituteDataAttribute、Frozen 機制、Greedy 建構策略。包含 IMapper (AutoMapper/Mapster) 等特殊相依性的客製化處理。適用於複雜相依性注入測試、Mock 驗證與行為設定。 Keywords: autofixture nsubstitute, auto mocking, AutoNSubstituteDataAttribute, 自動模擬, Frozen, AutoNSubstituteCustomiz...
Full skill instructions
本技能介紹如何整合 AutoFixture 與 NSubstitute,透過 AutoFixture.AutoNSubstitute 套件實現自動模擬(Auto-Mocking)功能。這種整合方式可以大幅簡化具有多個相依性的服務類別測試,讓開發者專注於測試邏輯本身,而非繁瑣的物件建立過程。
當被要求執行以下任務時,請使用此技能:
[Frozen] 屬性確保相依性實例在測試中保持一致Substitute.For<T>()# 核心套件
dotnet add package AutoFixture.AutoNSubstitute
# 相關套件(如尚未安裝)
dotnet add package AutoFixture
dotnet add package AutoFixture.Xunit2
dotnet add package NSubstitute
dotnet add package xunit
| 套件名稱 | 用途 | NuGet 連結 |
|---|---|---|
AutoFixture.AutoNSubstitute | AutoFixture 與 NSubstitute 整合 | nuget.org |
AutoFixture.Xunit2 | xUnit 整合(AutoData 屬性) | nuget.org |
NSubstitute | 模擬框架 | nuget.org |
當在 AutoFixture 中加入 AutoNSubstituteCustomization 時,它會自動:
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>();
[Frozen] 屬性用來控制測試中某個類型的實例:
[Frozen] 時,AutoFixture 會建立這個類別的一個實例並凍結它[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) // 太晚凍結了
[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>() 呼叫[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();
}
優勢:
在實際專案中,通常需要整合多種客製化設定:
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;
}
}
用於結合固定測試值與自動產生物件:
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 繼承自 CompositeDataAttributeAutoDataAttribute 實例作為資料來源提供者AutoDataWithCustomizationAttribute 的所有設定某些相依性不適合使用 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?
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);
}
結合固定測試值與自動產生的 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);
}
[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);
}
當需要精確控制測試資料時:
[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();
}
測試 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 值null!:告訴編譯器這是刻意的測試資料! 運算子:在測試中使用 null-forgiving 運算子| 場景 | 原因 |
|---|---|
| 服務層測試 | 通常有多個相依性,自動模擬效益最大 |
| 複雜相依圖 | AutoFixture 自動處理多層相依性 |
| 參數化測試 | 結合固定值與自動產生資料 |
| 需要大量測試資料 | 減少手動建立測試資料的工作 |
| 快速迭代開發 | 建構函式變更時測試通常不需修改 |
| 場景 | 原因 |
|---|---|
| 單一相依性測試 | 手動建立可能更清晰直覺 |
| 精確控制屬性值 | 需要額外的 fixture.Build().With() 設定 |
| 團隊不熟悉 AutoFixture | 學習成本可能影響開發效率 |
| 除錯困難的場景 | 自動產生的物件可能讓除錯變複雜 |
| 效能敏感的測試 | 物件建立的開銷可能影響執行速度 |
漸進式採用
團隊培訓
建立規範
MyProject.Tests/
├── AutoFixtureConfigurations/
│ ├── AutoDataWithCustomizationAttribute.cs
│ ├── InlineAutoDataWithCustomizationAttribute.cs
│ ├── AutoMapperCustomization.cs
│ └── DomainCustomization.cs
├── Services/
│ ├── OrderServiceTests.cs
│ └── ShipperServiceTests.cs
└── ...
[專案名稱]AutoDataAttribute 或 AutoDataWithCustomizationAttribute[功能]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);
}
[ClassData] 或 IClassFixture<T> 共享設定| 技能名稱 | 關聯說明 |
|---|---|
autofixture-basics | AutoFixture 基礎使用,本技能的前置知識 |
autofixture-customization | 自訂 Customization 的進階用法 |
autodata-xunit-integration | AutoData 屬性家族的完整說明 |
nsubstitute-mocking | NSubstitute 基礎,Mock 設定的詳細說明 |
本技能內容提煉自「老派軟體工程師的測試修練 - 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