AutoFixture 基礎:自動產生測試資料
安裝套件
<PackageReference Include="AutoFixture" Version="4.18.1" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.18.1" />
或透過命令列安裝:
dotnet add package AutoFixture
dotnet add package AutoFixture.Xunit2
基本使用方式
Fixture 類別與 Create<T>()
Fixture 是 AutoFixture 的核心類別,提供自動產生測試資料的能力:
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>() 產生集合
[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 能夠自動建構複雜的物件結構:
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>() 模式:
[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() 控制自動設定
[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
// 預設會拋出例外
[Fact]
public void 循環參考_預設行為_拋出例外()
{
var fixture = new Fixture();
// Category 有 Parent 屬性指向自己,造成循環參考
Action act = () => fixture.Create<Category>();
act.Should().Throw<ObjectCreationException>();
}
OmitOnRecursionBehavior:忽略循環參考
[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();
}
共用基底類別
建議建立基底類別來統一處理循環參考:
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 共享客製化
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 測試
[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;
}
}
匿名測試原則
核心概念
測試應該關注「行為」而不是「資料」。在大多數情況下,我們並不在乎具體的資料值是什麼:
// ✅ 好的做法:專注於測試邏輯
[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)
// 需要手動建立 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)
// 零設定成本,專注於測試邏輯 (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
最佳實踐
應該做
- 使用匿名測試概念 - 專注於測試邏輯而非具體資料
- 只在必要時固定特定值 - 使用
Build<T>().With()設定關鍵屬性 - 建立共用基底類別 - 統一處理循環參考等共同配置
- 合理的集合大小 - 根據測試目的調整
CreateMany()數量
應該避免
- 過度依賴隨機值 - 不要假設隨機值的具體內容
- 忽略邊界值 - 仍需要明確測試邊界情況
- 濫用自動產生 - 簡單測試可能用固定值更清楚
混合策略建議
結合兩種方式的優點:
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 資料夾中的範例檔案:
- basic-autofixture-usage.cs - 基本 AutoFixture 使用方式
- complex-object-scenarios.cs - 複雜物件與循環參考處理
- xunit-integration.cs - xUnit 整合與 Theory 測試
輸出格式
- 產生使用 AutoFixture 的測試類別(.cs 檔案)
- 包含 Fixture.Create/CreateMany/Build 用法範例
- 提供 .csproj 套件參考(AutoFixture、AutoFixture.Xunit2)
- 包含循環參考處理與自訂行為設定
參考資源
原始文章
本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
- Day 10 - AutoFixture 基礎:自動產生測試資料
官方文件
相關技能
dotnet-testing-autofixture-customization- AutoFixture 進階自訂dotnet-testing-autofixture-bogus-integration- AutoFixture + Bogus 整合dotnet-testing-autofixture-nsubstitute-integration- AutoFixture + NSubstitute 整合dotnet-testing-autodata-xunit-integration- AutoData 與 xUnit 整合dotnet-testing-test-data-builder-pattern- Test Data Builder Pattern