dotnet-testing-test-data-builder-pattern

Test Data Builder Pattern 完整實作指南。當需要使用建構者模式建立可維護的測試資料或簡化複雜物件的測試準備時使用。涵蓋流暢介面、語意化方法、預設值設計與 Builder 組合模式。 Make sure to use this skill whenever the user mentions test data builder, builder pattern, fluent interface, Object Mother, or test data preparation for complex objects, even if they don't explicitly ask for the builder pattern. Keywords: test data builder, builder pattern test, 測試資料建構器, object mother, fluent interface, 流暢介面, UserBuilder, ProductBuilder, .With(), .Build(), AUser(), 測試資料準備, 複雜物件建立, 語意化測試

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "dotnet-testing-test-data-builder-pattern" with this command: npx skills add kevintsengtw/dotnet-testing-agent-skills/kevintsengtw-dotnet-testing-agent-skills-dotnet-testing-test-data-builder-pattern

Test Data Builder Pattern 測試資料建構器模式

核心概念

什麼是 Test Data Builder Pattern?

Test Data Builder Pattern 是 Object Mother Pattern 的改良版,主要解決以下問題:

  1. 固定測試資料的問題:Object Mother 提供固定的測試物件,難以針對特定測試情境調整
  2. 測試意圖不明確:直接建立物件時,測試的關注點容易被大量屬性設定所掩蓋
  3. 重複程式碼:相似的物件建立邏輯在多個測試中重複出現

為何需要 Builder Pattern?

傳統測試資料建立的問題:

// ❌ 問題:過多參數設定,測試意圖不明確
var user = new User
{
    Name = "John Doe",
    Email = "john@example.com",
    Age = 30,
    Roles = new[] { "User" },
    Settings = new UserSettings { Theme = "Dark", Language = "zh-TW" },
    IsActive = true,
    CreatedAt = DateTime.Now,
    ModifiedAt = DateTime.Now
};

使用 Builder Pattern 的改善:

// ✅ 改善:意圖明確,只設定測試關注的屬性
var user = UserBuilder
    .AUser()
    .WithName("John Doe")
    .WithValidEmail()
    .Build();

實作指南

基本 Builder 結構

一個標準的 Test Data Builder 應包含:

  1. 預設值:為所有必要屬性提供合理的預設值
  2. 流暢介面:使用 With* 方法鏈來設定屬性
  3. 語意化方法:提供有意義的預設建立者(如 AnAdminUser()ARegularUser()
  4. Build 方法:最終建立並回傳目標物件

完整 Builder 範例

public class UserBuilder
{
    // 預設值:提供所有屬性的合理預設值
    private string _name = "Default User";
    private string _email = "default@example.com";
    private int _age = 25;
    private List<string> _roles = new();
    private UserSettings _settings = new()
    {
        Theme = "Light",
        Language = "en-US"
    };
    private bool _isActive = true;
    private DateTime _createdAt = DateTime.UtcNow;

    // With* 方法:流暢介面設定個別屬性
    public UserBuilder WithName(string name)
    {
        _name = name;
        return this;
    }

    public UserBuilder WithEmail(string email)
    {
        _email = email;
        return this;
    }

    public UserBuilder WithAge(int age)
    {
        _age = age;
        return this;
    }

    public UserBuilder WithRole(string role)
    {
        _roles.Add(role);
        return this;
    }

    public UserBuilder WithRoles(params string[] roles)
    {
        _roles.AddRange(roles);
        return this;
    }

    public UserBuilder WithSettings(UserSettings settings)
    {
        _settings = settings;
        return this;
    }

    public UserBuilder IsInactive()
    {
        _isActive = false;
        return this;
    }

    public UserBuilder CreatedOn(DateTime createdAt)
    {
        _createdAt = createdAt;
        return this;
    }

    // 語意化預設建立者:提供常見情境的快速建立方法
    public static UserBuilder AUser() => new();

    public static UserBuilder AnAdminUser() => new UserBuilder()
        .WithRoles("Admin", "User");

    public static UserBuilder ARegularUser() => new UserBuilder()
        .WithRole("User");

    public static UserBuilder AnInactiveUser() => new UserBuilder()
        .IsInactive();

    // 語意化組合方法
    public UserBuilder WithValidEmail()
    {
        _email = $"{_name.Replace(" ", ".").ToLower()}@example.com";
        return this;
    }

    public UserBuilder WithAdminRights()
    {
        return WithRoles("Admin", "User");
    }

    // Build 方法:建立最終物件
    public User Build()
    {
        return new User
        {
            Name = _name,
            Email = _email,
            Age = _age,
            Roles = _roles.ToArray(),
            Settings = _settings,
            IsActive = _isActive,
            CreatedAt = _createdAt,
            ModifiedAt = _createdAt
        };
    }
}

在測試中使用 Builder

單一測試情境

[Fact]
public void CreateUser_有效管理員使用者_應成功建立()
{
    // Arrange - 使用 Builder 建立測試資料
    var adminUser = UserBuilder
        .AnAdminUser()
        .WithName("John Admin")
        .WithEmail("john.admin@company.com")
        .WithAge(35)
        .Build();

    var userService = new UserService();

    // Act
    var result = userService.CreateUser(adminUser);

    // Assert
    Assert.NotNull(result);
    Assert.Equal("John Admin", result.Name);
    Assert.Contains("Admin", result.Roles);
}

配合 Theory 使用

public class UserValidationTests
{
    [Theory]
    [MemberData(nameof(GetUserScenarios))]
    public void ValidateUser_不同使用者情境_應回傳正確驗證結果(User user, bool expected)
    {
        // Arrange
        var validator = new UserValidator();

        // Act
        var result = validator.IsValid(user);

        // Assert
        Assert.Equal(expected, result);
    }

    public static IEnumerable<object[]> GetUserScenarios()
    {
        // ✅ 有效使用者情境
        yield return new object[]
        {
            UserBuilder.AUser()
                .WithName("Valid User")
                .WithEmail("valid@example.com")
                .WithAge(25)
                .Build(),
            true
        };

        // ❌ 無效使用者情境 - 空名稱
        yield return new object[]
        {
            UserBuilder.AUser()
                .WithName("")
                .Build(),
            false
        };

        // ❌ 無效使用者情境 - 年齡過小
        yield return new object[]
        {
            UserBuilder.AUser()
                .WithAge(10)
                .Build(),
            false
        };

        // ❌ 無效使用者情境 - 無效 Email
        yield return new object[]
        {
            UserBuilder.AUser()
                .WithEmail("invalid-email")
                .Build(),
            false
        };
    }
}

最佳實踐

1. 提供合理的預設值

良好實踐:預設值讓物件處於有效狀態

public class ProductBuilder
{
    private string _name = "Default Product";
    private decimal _price = 100m;
    private int _stock = 10;
    private bool _isAvailable = true;

    // 預設值確保建立的物件是有效的
    public Product Build() => new()
    {
        Name = _name,
        Price = _price,
        Stock = _stock,
        IsAvailable = _isAvailable
    };
}

2. 使用語意化的命名

良好實踐:方法名稱表達測試意圖

public static class UserScenarios
{
    public static UserBuilder ANewUser() => UserBuilder.AUser()
        .CreatedOn(DateTime.UtcNow);

    public static UserBuilder AnExpiredUser() => UserBuilder.AUser()
        .CreatedOn(DateTime.UtcNow.AddYears(-5))
        .IsInactive();

    public static UserBuilder APremiumUser() => UserBuilder.AUser()
        .WithRoles("Premium", "User")
        .WithSettings(new UserSettings { FeatureFlags = new[] { "AdvancedSearch" } });
}

3. Builder 之間的組合

良好實踐:Builder 可以組合使用

public class OrderBuilder
{
    private User _customer = UserBuilder.AUser().Build();
    private List<Product> _products = new();
    private decimal _totalAmount = 0m;

    public OrderBuilder ForCustomer(User customer)
    {
        _customer = customer;
        return this;
    }

    public OrderBuilder WithProduct(Product product)
    {
        _products.Add(product);
        _totalAmount += product.Price;
        return this;
    }

    public OrderBuilder WithProducts(params Product[] products)
    {
        _products.AddRange(products);
        _totalAmount = _products.Sum(p => p.Price);
        return this;
    }

    public Order Build() => new()
    {
        Customer = _customer,
        Products = _products,
        TotalAmount = _totalAmount,
        OrderDate = DateTime.UtcNow
    };
}

// 使用組合的 Builder
var order = new OrderBuilder()
    .ForCustomer(UserBuilder.APremiumUser().Build())
    .WithProducts(
        ProductBuilder.AProduct().WithPrice(100m).Build(),
        ProductBuilder.AProduct().WithPrice(200m).Build()
    )
    .Build();

4. 避免過度複雜化

不良實踐:Builder 過於複雜

// 避免在 Builder 中加入複雜的業務邏輯
public UserBuilder WithComplexValidation()
{
    // ❌ 不要在 Builder 中進行複雜驗證
    if (_email.Contains("@"))
    {
        var parts = _email.Split('@');
        if (parts[1].Length > 10)
        {
            _email = parts[0] + "@short.com";
        }
    }
    return this;
}

良好實踐:保持 Builder 簡單

// Builder 只負責建立物件,不包含業務邏輯
public UserBuilder WithShortDomainEmail()
{
    _email = "user@short.com";
    return this;
}

5. 統一管理測試資料

良好實踐:建立共享的測試資料類別

public static class TestData
{
    public static class Users
    {
        public static User John => UserBuilder.AUser()
            .WithName("John Doe")
            .WithEmail("john@example.com")
            .Build();

        public static User AdminUser => UserBuilder.AnAdminUser()
            .WithName("Admin User")
            .WithEmail("admin@company.com")
            .Build();
    }

    public static class Products
    {
        public static Product Laptop => ProductBuilder.AProduct()
            .WithName("Laptop")
            .WithPrice(1000m)
            .Build();
    }
}

// 在測試中使用
[Fact]
public void ProcessOrder_有效訂單_應成功處理()
{
    var order = new OrderBuilder()
        .ForCustomer(TestData.Users.John)
        .WithProduct(TestData.Products.Laptop)
        .Build();
    // ...
}

與其他模式的比較

Test Data Builder vs. Object Mother

特性Test Data BuilderObject Mother
彈性高度彈性,可針對測試調整固定的測試資料
可讀性流暢介面,意圖明確需要查看方法實作
維護性集中管理,易於修改變更影響所有測試
使用場景單元測試、情境測試簡單的整合測試

Test Data Builder vs. AutoFixture

特性Test Data BuilderAutoFixture
控制度完全控制物件建立自動產生,控制度較低
設定複雜度需手動建立 Builder幾乎零設定
測試意圖非常明確需額外說明
適用時機需要精確控制的測試大量資料產生、匿名測試

建議:Test Data Builder 和 AutoFixture 可以相輔相成。簡單情境使用 AutoFixture,複雜情境或需明確意圖時使用 Builder Pattern。

實戰範例

請參考 templates/ 目錄下的完整實作範例:

  • user-builder-example.cs - 基本 User Builder 實作
  • advanced-builder-scenarios.cs - 進階 Builder 組合與使用情境
  • builder-with-theory.cs - Builder 配合 xUnit Theory 的實務範例

輸出格式

  • 產生 Builder 類別檔案(如 UserBuilder.csProductBuilder.cs),放置於測試專案的 Builders/ 目錄
  • 每個 Builder 包含預設值、With* 流暢方法、語意化靜態工廠方法與 Build() 方法
  • 若需要跨測試共用,產生 TestData.cs 靜態類別集中管理常用測試資料
  • 產生對應的測試類別檔案(*Tests.cs),示範 Builder 的使用方式

參考資源

原始文章

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

延伸閱讀

相關技能

  • autofixture-basics - 使用 AutoFixture 自動產生測試資料
  • xunit-project-setup - xUnit 測試專案的基礎設定
  • test-naming-conventions - 測試命名規範

總結

Test Data Builder Pattern 是撰寫可維護測試的重要技巧:

  • 使用時機:測試物件有多個屬性、需重複使用測試資料、希望表達清晰意圖
  • 核心優勢:提升可讀性、降低維護成本、增強表達力
  • 注意事項:保持 Builder 簡單、提供合理預設值、使用語意化方法名稱

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

dotnet-testing-advanced-webapi-integration-testing

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

dotnet-testing-unit-test-fundamentals

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

dotnet-testing-xunit-project-setup

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

dotnet-testing-advanced-aspnet-integration-testing

No summary provided by upstream source.

Repository SourceNeeds Review
dotnet-testing-test-data-builder-pattern | V50.AI