skills/kevintsengtw/dotnet-testing-agent-skills/dotnet-testing-autofixture-bogus-integration

dotnet-testing-autofixture-bogus-integration

SKILL.md

AutoFixture 與 Bogus 整合應用指南

核心概念

為什麼需要整合?

AutoFixture 的優勢

  • 快速產生匿名測試資料
  • 自動處理複雜物件結構
  • 良好的循環參考處理機制

Bogus 的優勢

  • 產生真實感的語意化資料
  • 豐富的資料類型支援(Email、Phone、Address 等)
  • 對驗證比較友善的資料格式

整合後的效果

// 整合前的問題
var user = fixture.Create<User>();
// user.Email 可能是 "Email1a2b3c4d",不像真實 Email

// 整合後
var user = integratedFixture.Create<User>();
// user.Email 是 "john.doe@example.com"
// user.FirstName 是 "John"
// 其他屬性由 AutoFixture 自動填充

套件安裝

<PackageReference Include="AutoFixture" Version="4.18.1" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageReference Include="Bogus" Version="35.6.3" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="AwesomeAssertions" Version="9.1.0" />

整合架構

整合方式總覽

整合方式 適用場景 複雜度
屬性層級 SpecimenBuilder 特定屬性使用 Bogus
類型層級 SpecimenBuilder 整個類型使用 Bogus
混合產生器 (HybridGenerator) 統一 API 整合
整合工廠 (IntegratedFactory) 完整測試場景建構
自訂 AutoData 屬性 xUnit 整合

核心整合技術

透過 ISpecimenBuilder 介面實現屬性層級與類型層級的整合,搭配擴充方法(WithBogus()WithOmitOnRecursion()WithSeed())簡化設定流程。涵蓋 Email、Phone、Name、Address 等常用 SpecimenBuilder 以及完整的類型產生器註冊模式。

完整內容請參閱 references/core-integration-techniques.md


循環參考處理

為什麼循環參考很重要?

public class User
{
    public Company? Company { get; set; }  // User 參考 Company
}

public class Company
{
    public List<User> Employees { get; set; } = new();  // Company 參考 User
}

問題:User → Company → Employees(User) → Company → ... 無限循環

解決方案:OmitOnRecursionBehavior

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

效果

  • 避免 StackOverflowException
  • 循環參考的屬性設為 null 或空集合
  • 某些深層屬性可能為 null(這是預期行為)

自訂 AutoData 屬性

BogusAutoDataAttribute

public class BogusAutoDataAttribute : AutoDataAttribute
{
    public BogusAutoDataAttribute()
        : base(() => new Fixture().WithBogus())
    {
    }
}

使用方式

[Theory]
[BogusAutoData]
public void 使用整合資料測試(User user, Address address)
{
    user.Email.Should().Contain("@");
    user.FirstName.Should().NotBeNullOrEmpty();
    address.City.Should().NotBeNullOrEmpty();
}

混合產生器

ITestDataGenerator 介面

public interface ITestDataGenerator
{
    T Generate<T>();
    IEnumerable<T> Generate<T>(int count);
    T Generate<T>(Action<T> configure);
}

HybridTestDataGenerator 實作

public class HybridTestDataGenerator : ITestDataGenerator
{
    private readonly IFixture _fixture;

    public HybridTestDataGenerator(int? seed = null)
    {
        _fixture = new Fixture()
            .WithBogus()
            .WithOmitOnRecursion();

        if (seed.HasValue)
        {
            Bogus.Randomizer.Seed = new Random(seed.Value);
        }
    }

    public T Generate<T>() => _fixture.Create<T>();

    public IEnumerable<T> Generate<T>(int count)
        => Enumerable.Range(0, count).Select(_ => Generate<T>());

    public T Generate<T>(Action<T> configure)
    {
        var item = Generate<T>();
        configure(item);
        return item;
    }
}

整合測試資料工廠

IntegratedTestDataFactory

public class IntegratedTestDataFactory
{
    private readonly IFixture _fixture;
    private readonly Dictionary<Type, object> _cache = new();

    public IntegratedTestDataFactory(int? seed = null)
    {
        _fixture = new Fixture()
            .WithBogus()
            .WithOmitOnRecursion()
            .WithRepeatCount(3);

        if (seed.HasValue)
        {
            _fixture.WithSeed(seed.Value);
        }
    }

    public T CreateFresh<T>() => _fixture.Create<T>();

    public List<T> CreateMany<T>(int count = 3)
        => _fixture.CreateMany<T>(count).ToList();

    public T GetCached<T>() where T : class
    {
        var type = typeof(T);
        if (_cache.TryGetValue(type, out var cached))
            return (T)cached;

        var instance = CreateFresh<T>();
        _cache[type] = instance;
        return instance;
    }

    public void ClearCache() => _cache.Clear();

    /// <summary>
    /// 建立完整測試場景
    /// </summary>
    public TestScenario CreateTestScenario()
    {
        var company = CreateFresh<Company>();
        var users = CreateMany<User>(5);
        var orders = CreateMany<Order>(10);

        // 建立關聯
        foreach (var user in users)
        {
            user.Company = company;
        }

        company.Employees = users;

        return new TestScenario
        {
            Company = company,
            Users = users,
            Orders = orders
        };
    }
}

測試基底類別

TestBase 實作

public abstract class TestBase
{
    protected readonly IFixture Fixture;
    protected readonly HybridTestDataGenerator Generator;
    protected readonly IntegratedTestDataFactory Factory;

    protected TestBase(int? seed = null)
    {
        Fixture = new Fixture()
            .WithBogus()
            .WithOmitOnRecursion()
            .WithRepeatCount(3);

        if (seed.HasValue)
        {
            Fixture.WithSeed(seed.Value);
        }

        Generator = new HybridTestDataGenerator(seed);
        Factory = new IntegratedTestDataFactory(seed);
    }

    protected T Create<T>() => Fixture.Create<T>();

    protected List<T> CreateMany<T>(int count = 3)
        => Fixture.CreateMany<T>(count).ToList();

    protected T Create<T>(Action<T> configure)
    {
        var instance = Create<T>();
        configure(instance);
        return instance;
    }
}

Seed 管理與可重現性

重要限制

由於 AutoFixture 和 Bogus 有不同的隨機數管理機制:

  • Seed 確保測試行為穩定性
  • Seed 確保資料格式一致性
  • 無法保證所有屬性值完全相同

建議做法

// 使用 Seed 確保穩定性
var factory = new IntegratedTestDataFactory(seed: 12345);

// 如果需要完全可重現,使用單一工具
var faker = new Faker<User>();
faker.UseSeed(12345);

使用範例

基本整合使用

[Fact]
public void AutoFixture_整合_Bogus_應能產生真實感資料()
{
    // Arrange
    var fixture = new Fixture().WithBogus();

    // Act
    var user = fixture.Create<User>();

    // Assert
    user.Email.Should().Contain("@");
    user.FirstName.Should().NotBeNullOrEmpty();
    user.Phone.Should().MatchRegex(@"[\d\-\(\)\s]+");
}

使用工廠建立測試場景

[Fact]
public void 工廠_應能建立完整的測試場景()
{
    // Arrange
    var factory = new IntegratedTestDataFactory(seed: 42);

    // Act
    var scenario = factory.CreateTestScenario();

    // Assert
    scenario.Company.Should().NotBeNull();
    scenario.Users.Should().HaveCount(5);
    scenario.Orders.Should().HaveCount(10);

    scenario.Users.Should().AllSatisfy(user =>
    {
        user.Company.Should().Be(scenario.Company);
        user.Email.Should().Contain("@");
    });
}

最佳實踐

建議做法

  1. 永遠先處理循環參考

    fixture.WithOmitOnRecursion().WithBogus();
    
  2. 為常用實體建立專用 SpecimenBuilder

  3. 使用 Seed 確保測試穩定性

  4. 建立測試基底類別統一資料產生邏輯

  5. 適當使用快取提升效能

避免事項

  1. 過度設計,保持簡單實用
  2. 期望整合環境完全可重現
  3. 忽略循環參考處理
  4. 在每個測試中重新建立 Fixture

整合與 AutoFixture/Bogus 的比較

面向 純 AutoFixture 純 Bogus 整合方案
資料真實感
設定複雜度
物件關聯處理 自動 手動 自動
循環參考處理 內建 整合
可重現性
適用場景 單元測試 整合測試/原型 兩者皆可

輸出格式

  • 產生 ISpecimenBuilder 實作類別檔案(如 EmailSpecimenBuilder.cs
  • 產生 Fixture 擴充方法類別檔案(FixtureBogusExtensions.cs
  • 產生整合測試資料工廠或混合產生器類別
  • 產生 BogusAutoDataAttribute 供 xUnit Theory 測試使用
  • 搭配 OmitOnRecursionBehavior 處理循環參考

參考資源

原始文章

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

官方文件


相關技能

Weekly Installs
19
GitHub Stars
18
First Seen
Jan 24, 2026
Installed on
gemini-cli16
claude-code15
opencode14
antigravity13
github-copilot13
codex13