SuperAPI AI Directory

dotnet-testing-autofixture-nsubstitute-integration

Testing & quality

View repository

Updated 10-15 18:02

About this skill

AutoFixture + NSubstitute Auto-Mocking Integration

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:

  • InlineAutoDataAttribute inherits from CompositeDataAttribute
  • It needs to receive an AutoDataAttribute instance as a data source provider
  • This allows reuse of AutoDataWithCustomizationAttribute all 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

  1. Gradual adoption

    • Start with simple service classes
    • Gradually expand to complex scenarios
    • Let the team gradually become familiar with the patterns
  2. Team training

    • Ensure the team understands the Frozen mechanism
    • Explain the importance of parameter order
    • Share debugging tips
  3. 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:[專案名稱]AutoDataAttribute or AutoDataWithCustomizationAttribute
  • Customization classes:[功能]Customization(such as MapsterMapperCustomization)
  • Test methods: maintain 方法_情境_預期 naming pattern

Notes and Limitations

Common Pitfalls

  1. Incorrect parameter order

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

    // 沒有 AutoNSubstitute,介面會產生異常
    var fixture = new Fixture();
    
    // 加入 AutoNSubstituteCustomization
    var fixture = new Fixture().Customize(new AutoNSubstituteCustomization());
    
  3. 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] or IClassFixture<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 AutoDataAttribute derived class file (*AutoDataAttribute.cs)
  • Generate ICustomization implementation class file (*Customization.cs)
  • Test method uses [Theory] with custom AutoData attribute
  • [Frozen] Parameters placed before SUT parameters
  • with NSubstitute's Returns() and Received() 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:

Official documentation

Further reading

Sample code