Core Value
- Reduce boilerplate code: No need to manually create for each interface
Substitute.For<T>() - Automatically handle complex dependency graphs: AutoFixture automatically resolves and creates the required objects
- Improve test maintainability: When constructors change, test code usually does not need to be updated accordingly
- Keep tests focused: Let developers focus on test logic rather than object creation
Package Installation and Setup
Required Packages
# 核心套件
dotnet add package AutoFixture.AutoNSubstitute
Related Packages (if not yet installed)
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 Freezing Mechanism
[Frozen] The attribute is used to control an instance of a certain type in a test:
- When a parameter is annotated with
[Frozen], AutoFixture creates an instance of this class andfreezesit - Subsequently, the same frozen instance will be used in the test method
- This is especially important for tests that need to configure dependency behavior and then verify the 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();
}
Importance of Parameter Order
When using [Frozen] ,parameter order is very important:
// 正確:Frozen 參數在 SUT 之前
public async Task TestMethod(
[Frozen] IRepository repository,
MyService sut)
// 錯誤:SUT 會使用不同的 repository 實例
public async Task TestMethod(
MyService sut,
[Frozen] IRepository repository) // 太晚凍結了
Traditional Approach vs AutoNSubstitute Approach
Traditional Manual Approach
[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();
}
Problem:
- When a service adds a new dependency, all tests need to be modified
- A large amount of repeated
Substitute.For<T>()calls - Test code is verbose and difficult to quickly understand the test intent
Using the AutoNSubstitute Approach
[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();
}
Advantages:
- Only declare the dependencies that need interaction
- Other dependencies (logger, notificationService, cacheService) are created automatically
- When constructors change, tests usually do not need to be modified
Custom AutoData Attribute
Why do you need a custom AutoData attribute?
In real projects, it is usually necessary to integrate multiple custom settings:
- AutoNSubstituteCustomization: Automatically create NSubstitute substitutes for interfaces
- Project-specific Customization: Such as Mapper settings, validator settings, etc.
- Consistent test infrastructure: Ensure the entire project uses the same settings
AutoDataWithCustomizationAttribute Implementation
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 Implementation
Used to combine fixed test values with automatically generated objects:
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)
{
}
}
Important Implementation Details
Why use new AutoDataWithCustomizationAttribute() instead of the CreateFixture method?
// 錯誤:InlineAutoDataAttribute 需要 AutoDataAttribute,不是 Func<IFixture>
public InlineAutoDataWithCustomizationAttribute(params object[] values)
: base(CreateFixture, values) // 編譯錯誤或行為異常
// 正確:傳遞 AutoDataAttribute 實例
public InlineAutoDataWithCustomizationAttribute(params object[] values)
: base(new AutoDataWithCustomizationAttribute(), values)
Reason:
InlineAutoDataAttributeinherits fromCompositeDataAttribute- It needs to receive an
AutoDataAttributeinstance as a data source provider - This allows reuse of
AutoDataWithCustomizationAttributeall settings
Customization Handling for Common Dependencies
Some dependencies (such as IMapper) are not suitable for Mocking and should use real instances instead. Includes customization examples for Mapster and AutoMapper.
For a complete customization handling example, refer to references/dependency-customization.md
Test Implementation Example
Complete examples covering basic tests, Frozen dependency behavior configuration, automatic test data generation, InlineAutoData parameterized tests, CollectionSize control, IFixture complex data configuration, Nullable reference type handling, and more.
For complete test implementation examples, refer to references/test-implementation-examples.md
Scenario Suitability Assessment
Recommended Scenarios
| Scenario | Reason |
|---|---|
| Service layer testing | Usually has multiple dependencies, maximizing the benefit of automatic mocking |
| Complex dependency graphs | AutoFixture automatically handles multi-level dependencies |
| Parameterized testing | Combines fixed values with automatically generated data |
| Need for large amounts of test data | Reduces the work of manually creating test data |
| Rapid iterative development | Tests usually do not need modification when constructors change |
Scenarios to Use with Caution
| Scenario | Reason |
|---|---|
| Single dependency testing | Manual creation may be clearer and more intuitive |
| Precise control over property values | Requires additional fixture.Build().With() configuration |
| Team is unfamiliar with AutoFixture | Learning cost may affect development efficiency |
| Scenarios where debugging is difficult | Automatically generated objects may complicate debugging |
| Performance-sensitive tests | Object creation overhead may affect execution speed |
Best Practices
Adoption Strategy
-
Gradual adoption
- Start with simple service classes
- Gradually expand to complex scenarios
- Let the team gradually become familiar with the patterns
-
Team training
- Ensure the team understands the Frozen mechanism
- Explain the importance of parameter order
- Share debugging tips
-
Establish conventions
- When to use automatic generation vs manual creation
- Naming and organization of custom Customizations
- Control strategies for test data
Code organization
MyProject.Tests/
├── AutoFixtureConfigurations/
│ ├── AutoDataWithCustomizationAttribute.cs
│ ├── InlineAutoDataWithCustomizationAttribute.cs
│ ├── AutoMapperCustomization.cs
│ └── DomainCustomization.cs
├── Services/
│ ├── OrderServiceTests.cs
│ └── ShipperServiceTests.cs
└── ...
Naming conventions
- Custom AutoData attributes:
[專案名稱]AutoDataAttributeorAutoDataWithCustomizationAttribute - Customization classes:
[功能]Customization(such asMapsterMapperCustomization) - Test methods: maintain
方法_情境_預期naming pattern
Notes and Limitations
Common Pitfalls
-
Incorrect parameter order
// Frozen 參數在 SUT 之後,不會生效 public void Test(MyService sut, [Frozen] IRepository repo) // Frozen 參數必須在 SUT 之前 public void Test([Frozen] IRepository repo, MyService sut) -
Forgetting AutoNSubstituteCustomization
// 沒有 AutoNSubstitute,介面會產生異常 var fixture = new Fixture(); // 加入 AutoNSubstituteCustomization var fixture = new Fixture().Customize(new AutoNSubstituteCustomization()); -
Over-reliance on automatic generation
// 測試意圖不明確 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); }
Performance considerations
- Each test method creates a new Fixture and all dependencies
- Complex object graphs may increase test execution time
- Consider using
[ClassData]orIClassFixture<T>to share configuration
Related Skills
| Skill Name | Relationship Description |
|---|---|
autofixture-basics |
Basic usage of AutoFixture, prerequisite knowledge for this skill |
autofixture-customization |
Advanced usage of custom Customizations |
autodata-xunit-integration |
Complete documentation of the AutoData attribute family |
nsubstitute-mocking |
NSubstitute basics, detailed explanation of Mock configuration |
Output Format
- Generate custom
AutoDataAttributederived class file (*AutoDataAttribute.cs) - Generate
ICustomizationimplementation class file (*Customization.cs) - Test method uses
[Theory]with custom AutoData attribute [Frozen]Parameters placed before SUT parameters- with NSubstitute's
Returns()andReceived()to perform behavior setup and verification
Reference resources
Original article
This skill content is distilled from the "Old-School Software Engineer's Testing Practice - 30-Day Challenge" series of articles:
- Day 13 - AutoFixture integration with NSubstitute: automatically creating mock objects
- Ironman series article:https://ithelp.ithome.com.tw/articles/10375419
- Sample code:https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day13
Official documentation
- AutoFixture.AutoNSubstitute NuGet Package
- AutoFixture Documentation - Auto Mocking
- NSubstitute Documentation
Further reading
Sample code
- custom-autodata-attributes.cs - Custom AutoData attribute template
- frozen-patterns.cs - Frozen mechanism usage pattern
- service-testing-examples.cs - Complete service layer testing example