home / skills / kevintsengtw / dotnet-testing-agent-skills / dotnet-testing-autofixture-basics

dotnet-testing-autofixture-basics skill

/skills/dotnet-testing-autofixture-basics

This skill streamlines .NET testing with AutoFixture for anonymous data generation, supports Create, CreateMany, Build, recursion handling, and xUnit

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

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

Files (5)
SKILL.md
13.7 KB
---
name: dotnet-testing-autofixture-basics
description: |
  使用 AutoFixture 自動產生測試資料的基礎技能。當需要快速產生測試物件、減少樣板程式碼、實現匿名測試時使用。涵蓋 Fixture.Create、CreateMany、循環參考處理、與 xUnit 整合等。
  Keywords: autofixture, fixture, 自動產生測試資料, test data generation, anonymous testing, 匿名測試, fixture.Create, CreateMany, fixture.Build, Create<T>, AutoFixture.Xunit2, OmitOnRecursionBehavior, IFixture, 產生測試資料, generate test data
license: MIT
metadata:
  author: Kevin Tseng
  version: "1.0.0"
  tags: ".NET, testing, AutoFixture, test data, anonymous testing"
  related_skills: "autodata-xunit-integration, autofixture-customization, autofixture-bogus-integration"
---

# AutoFixture 基礎:自動產生測試資料

## 適用情境

AutoFixture 是一個為 .NET 平台設計的測試資料自動產生工具,它的核心理念是「匿名測試」(Anonymous Testing)。這個概念認為,大部分的測試都不應該依賴於特定的資料值,而應該專注於驗證程式邏輯的正確性。

### 為什麼需要 AutoFixture?

傳統測試資料準備的痛點:

1. **樣板程式碼過多**:90% 的程式碼都在準備資料,真正的測試邏輯被埋沒
2. **測試焦點模糊**:很難快速理解這個測試在驗證什麼
3. **維護困難**:當物件結構改變時,所有相關測試都需要修改
4. **資料依賴性**:測試可能意外依賴於特定的資料值
5. **重複程式碼**:相同的資料準備邏輯在多個測試中重複出現

AutoFixture 可以看作是 **Test Data Builder Pattern 的自動化進化版**,能自動產生複雜的測試資料,讓我們專注於測試邏輯本身。

## 安裝套件

```xml
<PackageReference Include="AutoFixture" Version="4.18.1" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.18.1" />
```

或透過命令列安裝:

```powershell
dotnet add package AutoFixture
dotnet add package AutoFixture.Xunit2
```

## 基本使用方式

### Fixture 類別與 Create<T>()

`Fixture` 是 AutoFixture 的核心類別,提供自動產生測試資料的能力:

```csharp
using AutoFixture;

[Fact]
public void AutoFixture_基本使用_應產生有效資料()
{
    // Arrange
    var fixture = new Fixture();

    // Act - 產生基本型別
    var id = fixture.Create<int>();           // 隨機正整數
    var name = fixture.Create<string>();      // 類似 GUID 格式的字串
    var price = fixture.Create<decimal>();    // 隨機十進位數
    var isActive = fixture.Create<bool>();    // 隨機布林值
    var date = fixture.Create<DateTime>();    // 隨機日期時間
    var guid = fixture.Create<Guid>();        // 新的 GUID

    // Assert
    id.Should().BePositive();
    name.Should().NotBeNullOrEmpty();
    guid.Should().NotBe(Guid.Empty);
}
```

### CreateMany<T>() 產生集合

```csharp
[Fact]
public void CreateMany_產生集合_應有多個元素()
{
    var fixture = new Fixture();

    // 預設產生 3 個元素
    var products = fixture.CreateMany<Product>().ToList();
    
    // 指定數量
    var moreProducts = fixture.CreateMany<Product>(10).ToList();

    products.Should().HaveCount(3);
    moreProducts.Should().HaveCount(10);
}
```

### 複雜物件自動建構

AutoFixture 能夠自動建構複雜的物件結構:

```csharp
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public Address Address { get; set; }        // 巢狀物件
    public List<Order> Orders { get; set; }     // 集合屬性
}

[Fact]
public void 複雜物件_應完整建構所有層級()
{
    var fixture = new Fixture();

    var customer = fixture.Create<Customer>();

    // 所有屬性自動填入值
    customer.Should().NotBeNull();
    customer.Id.Should().BePositive();
    customer.Name.Should().NotBeNullOrEmpty();
    customer.Address.Should().NotBeNull();
    customer.Address.Street.Should().NotBeNullOrEmpty();
    customer.Orders.Should().NotBeEmpty();
}
```

## Build<T>() 模式:精確控制

當需要對特定屬性進行控制時,使用 `Build<T>()` 模式:

```csharp
[Fact]
public void Build模式_指定特定屬性()
{
    var fixture = new Fixture();

    var customer = fixture.Build<Customer>()
        .With(x => x.Name, "測試客戶")           // 指定固定值
        .With(x => x.Age, 25)                    // 指定固定值
        .Without(x => x.InternalId)              // 排除屬性
        .Create();

    customer.Name.Should().Be("測試客戶");
    customer.Age.Should().Be(25);
    customer.InternalId.Should().Be(default);
}
```

### OmitAutoProperties() 控制自動設定

```csharp
[Fact]
public void OmitAutoProperties_僅設定必要屬性()
{
    var fixture = new Fixture();

    var customer = fixture.Build<Customer>()
        .OmitAutoProperties()           // 不自動設定任何屬性
        .With(x => x.Id, 123)          // 只設定關心的屬性
        .With(x => x.Name, "測試客戶")
        .Create();

    customer.Id.Should().Be(123);
    customer.Name.Should().Be("測試客戶");
    customer.Email.Should().BeNullOrEmpty();  // 保持預設值
    customer.Age.Should().Be(0);              // 保持預設值
}
```

## 循環參考處理

當物件包含循環參考時,AutoFixture 提供兩種處理策略:

### 預設行為:ThrowingRecursionBehavior

```csharp
// 預設會拋出例外
[Fact]
public void 循環參考_預設行為_拋出例外()
{
    var fixture = new Fixture();
    
    // Category 有 Parent 屬性指向自己,造成循環參考
    Action act = () => fixture.Create<Category>();
    
    act.Should().Throw<ObjectCreationException>();
}
```

### OmitOnRecursionBehavior:忽略循環參考

```csharp
[Fact]
public void 循環參考_使用OmitOnRecursion_成功建立()
{
    var fixture = new Fixture();
    
    // 移除預設的拋出例外行為
    fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
        .ForEach(b => fixture.Behaviors.Remove(b));
    
    // 加入忽略循環參考行為
    fixture.Behaviors.Add(new OmitOnRecursionBehavior());

    var category = fixture.Create<Category>();

    category.Should().NotBeNull();
    category.Name.Should().NotBeNullOrEmpty();
}
```

### 共用基底類別

建議建立基底類別來統一處理循環參考:

```csharp
public abstract class AutoFixtureTestBase
{
    protected Fixture CreateFixture()
    {
        var fixture = new Fixture();

        fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
            .ForEach(b => fixture.Behaviors.Remove(b));
        fixture.Behaviors.Add(new OmitOnRecursionBehavior());

        return fixture;
    }
}

public class CustomerServiceTests : AutoFixtureTestBase
{
    [Fact]
    public void ProcessOrder_正常訂單_應處理成功()
    {
        var fixture = CreateFixture();
        var customer = fixture.Create<Customer>();
        
        // 測試邏輯...
    }
}
```

## xUnit 整合

### 使用 Fixture 共享客製化

```csharp
public class ProductServiceTests
{
    private readonly Fixture _fixture;

    public ProductServiceTests()
    {
        _fixture = new Fixture();
        
        // 共同的客製化設定
        _fixture.Customize<ProductCreateRequest>(c => c
            .With(x => x.Price, () => _fixture.Create<decimal>() % 10000)
            .With(x => x.Name, () => $"Product-{_fixture.Create<string>()[..8]}")
        );
    }

    [Fact]
    public void CreateProduct_使用共享Fixture_應成功建立()
    {
        var productData = _fixture.Create<ProductCreateRequest>();
        var service = new ProductService();

        var result = service.CreateProduct(productData);

        result.Should().NotBeNull();
        productData.Price.Should().BeLessThan(10000);
    }
}
```

### 結合 Theory 測試

```csharp
[Theory]
[InlineData(CustomerType.Regular)]
[InlineData(CustomerType.Premium)]
[InlineData(CustomerType.VIP)]
public void CalculateDiscount_不同客戶類型_應套用正確折扣(CustomerType customerType)
{
    var fixture = new Fixture();
    
    var customer = fixture.Build<Customer>()
        .With(x => x.Type, customerType)
        .Create();
        
    var order = fixture.Create<Order>();
    var calculator = new DiscountCalculator();

    var discount = calculator.Calculate(customer, order);

    switch (customerType)
    {
        case CustomerType.Regular:
            discount.Should().Be(0);
            break;
        case CustomerType.Premium:
            discount.Should().BeInRange(0.05m, 0.10m);
            break;
        case CustomerType.VIP:
            discount.Should().BeInRange(0.15m, 0.25m);
            break;
    }
}
```

## 匿名測試原則

### 核心概念

測試應該關注「行為」而不是「資料」。在大多數情況下,我們並不在乎具體的資料值是什麼:

```csharp
// ✅ 好的做法:專注於測試邏輯
[Fact]
public void AddCustomer_任何有效客戶_應成功新增()
{
    var fixture = new Fixture();
    var customer = fixture.Create<Customer>();
    var repository = new CustomerRepository();

    var result = repository.Add(customer);

    result.Should().BeTrue();
}

// ❌ 避免:依賴隨機值的具體內容
[Fact]
public void BadTest_依賴隨機值()
{
    var fixture = new Fixture();
    var customer = fixture.Create<Customer>();

    // 錯誤:假設隨機產生的年齡會大於 18
    customer.Age.Should().BeGreaterThan(18); // 可能失敗
}

// ✅ 正確:明確設定關鍵值
[Fact]
public void GoodTest_明確設定關鍵值()
{
    var fixture = new Fixture();
    var customer = fixture.Build<Customer>()
        .With(x => x.Age, 25)  // 明確設定
        .Create();

    var validator = new CustomerValidator();
    var isValid = validator.IsAdult(customer);

    isValid.Should().BeTrue();  // 穩定的結果
}
```

## 進化比較:Test Data Builder vs AutoFixture

### 傳統 Test Data Builder (Day 03)

```csharp
// 需要手動建立 Builder 類別 (40+ 行)
public class OrderBuilder
{
    private int _id = 1;
    private Customer _customer = new Customer { Name = "Default" };
    private List<OrderItem> _items = new();
    
    public OrderBuilder WithCustomer(Customer customer)
    {
        _customer = customer;
        return this;
    }
    
    public OrderBuilder WithItems(params OrderItem[] items)
    {
        _items = items.ToList();
        return this;
    }
    
    public Order Build() => new Order
    {
        Id = _id,
        Customer = _customer,
        Items = _items
    };
}
```

### AutoFixture 方式 (Day 10)

```csharp
// 零設定成本,專注於測試邏輯 (5 行)
var fixture = new Fixture();
var order = fixture.Build<Order>()
    .With(x => x.Status, OrderStatus.Completed)
    .Create();
```

### 比較摘要

| 層面       | Test Data Builder      | AutoFixture        |
| ---------- | ---------------------- | ------------------ |
| 程式碼行數 | 40+ 行 Builder + 測試  | 5 行測試           |
| 維護成本   | 物件改變需更新 Builder | 自動適應變化       |
| 開發時間   | 先寫 Builder 再寫測試  | 直接寫測試         |
| 大量資料   | 需要迴圈               | `CreateMany(100)`  |
| 可讀性     | 業務語意明確           | 需理解 AutoFixture |

## 實務應用場景

AutoFixture 在實務中常用於 Entity 測試(搭配 Theory 驗證不同輸入場景)、DTO 驗證(使用 `Build<T>()` 產生符合驗證規則的資料)、以及大量資料測試(使用 `CreateMany()` 產生批次資料)。

> 📖 完整程式碼範例請參閱 [references/practical-scenarios.md](references/practical-scenarios.md)

## 最佳實踐

### 應該做

1. **使用匿名測試概念** - 專注於測試邏輯而非具體資料
2. **只在必要時固定特定值** - 使用 `Build<T>().With()` 設定關鍵屬性
3. **建立共用基底類別** - 統一處理循環參考等共同配置
4. **合理的集合大小** - 根據測試目的調整 `CreateMany()` 數量

### 應該避免

1. **過度依賴隨機值** - 不要假設隨機值的具體內容
2. **忽略邊界值** - 仍需要明確測試邊界情況
3. **濫用自動產生** - 簡單測試可能用固定值更清楚

## 混合策略建議

結合兩種方式的優點:

```csharp
public static class TestDataFactory
{
    private static readonly Fixture _fixture = new();
    
    // 用 AutoFixture 建立基礎資料,再用 Builder 加工
    public static OrderBuilder AnOrder()
    {
        var baseOrder = _fixture.Create<Order>();
        return new OrderBuilder(baseOrder);
    }
    
    // 大量隨機資料產生
    public static IEnumerable<User> CreateRandomUsers(int count)
    {
        return _fixture.CreateMany<User>(count);
    }
}
```

## 程式碼範本

請參考 [templates](./templates) 資料夾中的範例檔案:

- [basic-autofixture-usage.cs](./templates/basic-autofixture-usage.cs) - 基本 AutoFixture 使用方式
- [complex-object-scenarios.cs](./templates/complex-object-scenarios.cs) - 複雜物件與循環參考處理
- [xunit-integration.cs](./templates/xunit-integration.cs) - xUnit 整合與 Theory 測試

## 參考資源

### 原始文章

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

- **Day 10 - AutoFixture 基礎:自動產生測試資料**
  - 鐵人賽文章:https://ithelp.ithome.com.tw/articles/10375018
  - 範例程式碼:https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day10

### 官方文件

- [AutoFixture GitHub](https://github.com/AutoFixture/AutoFixture)
- [AutoFixture 官方網站](https://autofixture.github.io/)
- [AutoFixture 快速入門](https://autofixture.github.io/docs/quick-start/)
- [AutoFixture NuGet](https://www.nuget.org/packages/autofixture)

Overview

This skill teaches the basics of using AutoFixture to generate test data automatically for .NET tests. It focuses on anonymous testing, reducing boilerplate, and producing complex objects and collections with Fixture.Create, CreateMany, Build, and xUnit integration. The content also covers recursion handling and practical best practices for stable tests.

How this skill works

AutoFixture's core class Fixture produces randomized instances for primitives and complex types using Create<T>() and CreateMany<T>(). Build<T>() lets you override or omit specific properties while preserving automatic generation for the rest. Recursion behaviors (ThrowingRecursionBehavior vs OmitOnRecursionBehavior) control how circular references are handled. Integration with xUnit and shared Fixture customizations let you reuse consistent generation rules across tests.

When to use it

  • When tests require many realistic but non-specific input values to focus on behavior.
  • To remove repetitive test data setup and reduce boilerplate in unit tests.
  • When you need bulk data for load or collection-based assertions (CreateMany).
  • When building complex nested objects or DTOs without manual construction.
  • When combining randomized base data with a few deliberate property values via Build<T>().

Best practices

  • Adopt anonymous testing: assert behavior, not specific random values.
  • Only set explicit values for properties that affect the behavior under test (Build<T>().With()).
  • Provide a shared base Fixture for common customizations and recursion handling.
  • Use OmitOnRecursionBehavior for domain models with circular references, or centralize recursion settings in a test base class.
  • Avoid relying on random value specifics; write explicit boundary tests where needed.

Example use cases

  • Generate many product DTOs with CreateMany<Product>(n) for batch validation tests.
  • Create a Customer with fixture.Create<Customer>() to test repository persistence behavior.
  • Use fixture.Build<Order>().With(x => x.Status, Completed).Create() to target business logic paths.
  • Share a customized Fixture in test class constructor to enforce consistent generation rules across tests.
  • Switch recursion behavior in a TestBase to safely create graph-like entities (Category with Parent).

FAQ

How do I avoid circular reference exceptions?

Remove ThrowingRecursionBehavior and add OmitOnRecursionBehavior on the Fixture, or centralize this change in a shared test base class.

When should I use Build<T>() vs Create<T>()?

Use Create<T>() for fully anonymized instances. Use Build<T>() when you need to fix or omit specific properties while letting AutoFixture populate the rest.