dotnet-testing-test-data-builder-pattern
npx skills add https://github.com/kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-test-data-builder-pattern
Agent 安装分布
Skill 文档
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 10 – AutoFixture åºç¤ï¼èªåç¢çæ¸¬è©¦è³æ (Builder Pattern æ¦å¿µ)
- éµäººè³½æç« ï¼https://ithelp.ithome.com.tw/articles/10375018
- ç¯ä¾ç¨å¼ç¢¼ï¼https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day10
延伸é±è®
- 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 ç°¡å®ï¼é¿å å å ¥æ¥åé輯
- æä¾åççé è¨å¼
- 使ç¨èªæåçæ¹æ³å稱