dotnet-testing-test-data-builder-pattern
SKILL.md
Test Data Builder Pattern 測試資料建構器模式
概述
Test Data Builder Pattern 是一種專為測試設計的建構者模式(Builder Pattern)變體,用於建立清晰、可維護且表意明確的測試資料。此模式特別適合處理具有多個屬性的複雜物件,讓測試程式碼更易讀且降低維護成本。
核心概念
什麼是 Test Data Builder Pattern?
Test Data Builder Pattern 是 Object Mother Pattern 的改良版,主要解決以下問題:
- 固定測試資料的問題:Object Mother 提供固定的測試物件,難以針對特定測試情境調整
- 測試意圖不明確:直接建立物件時,測試的關注點容易被大量屬性設定所掩蓋
- 重複程式碼:相似的物件建立邏輯在多個測試中重複出現
為何需要 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 應包含:
- 預設值:為所有必要屬性提供合理的預設值
- 流暢介面:使用
With*方法鏈來設定屬性 - 語意化方法:提供有意義的預設建立者(如
AnAdminUser()、ARegularUser()) - 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 Builder | Object Mother |
|---|---|---|
| 彈性 | ✅ 高度彈性,可針對測試調整 | ❌ 固定的測試資料 |
| 可讀性 | ✅ 流暢介面,意圖明確 | ⚠️ 需要查看方法實作 |
| 維護性 | ✅ 集中管理,易於修改 | ❌ 變更影響所有測試 |
| 使用場景 | 單元測試、情境測試 | 簡單的整合測試 |
Test Data Builder vs. AutoFixture
| 特性 | Test Data Builder | AutoFixture |
|---|---|---|
| 控制度 | ✅ 完全控制物件建立 | ⚠️ 自動產生,控制度較低 |
| 設定複雜度 | ⚠️ 需手動建立 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 的實務範例
參考資源
原始文章
本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
- Day 03 - 測試資料的準備方式:Test Data Builder Pattern
延伸閱讀
- Test Data Builder 原始文章:Test Data Builders: an alternative to the Object Mother pattern by Nat Pryce
- Builder Pattern vs Object Mother:Test Data Builders and Object Mother: another look
相關技能
autofixture-basics- 使用 AutoFixture 自動產生測試資料xunit-project-setup- xUnit 測試專案的基礎設定test-naming-conventions- 測試命名規範
總結
Test Data Builder Pattern 是撰寫可維護測試的重要技巧:
✅ 使用時機:
- 測試物件有多個屬性需要設定
- 需要在多個測試中重複使用相似的測試資料
- 希望測試程式碼表達清晰的意圖
✅ 核心優勢:
- 提升測試可讀性
- 降低測試維護成本
- 增強測試表達力
⚠️ 注意事項:
- 保持 Builder 簡單,避免加入業務邏輯
- 提供合理的預設值
- 使用語意化的方法名稱
Weekly Installs
6
Repository
kevintsengtw/dotnet-testing-agent-skillsInstalled on
claude-code5
antigravity4
gemini-cli4
windsurf3
opencode3
codex3