dotnet-testing-complex-object-comparison
npx skills add https://github.com/kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-complex-object-comparison
Agent 安装分布
Skill 文档
è¤éç©ä»¶æ¯å°æåï¼Complex Object Comparisonï¼
é©ç¨æ å¢
æ¤æè½å°æ³¨æ¼ .NET 測試ä¸çè¤éç©ä»¶æ¯å°å ´æ¯ï¼ä½¿ç¨ AwesomeAssertions ç BeEquivalentTo API èçå種é²éæ¯å°éæ±ã
æ ¸å¿ä½¿ç¨å ´æ¯
1. 深層ç©ä»¶çµæ§æ¯å° (Object Graph Comparison)
ç¶éè¦æ¯å°å å«å¤å±¤å·¢ç屬æ§çè¤éç©ä»¶æï¼
[Fact]
public void ComplexObject_æ·±å±¤çµæ§æ¯å°_æå®å
¨ç¸ç¬¦()
{
var expected = new Order
{
Id = 1,
Customer = new Customer
{
Name = "John Doe",
Address = new Address
{
Street = "123 Main St",
City = "Seattle",
ZipCode = "98101"
}
},
Items = new[]
{
new OrderItem { ProductName = "Laptop", Quantity = 1, Price = 999.99m },
new OrderItem { ProductName = "Mouse", Quantity = 2, Price = 29.99m }
}
};
var actual = orderService.GetOrder(1);
// 深層ç©ä»¶æ¯å°
actual.Should().BeEquivalentTo(expected);
}
2. 循ç°åç §èç (Circular Reference Handling)
èçç©ä»¶ä¹éåå¨å¾ªç°åç §çæ æ³ï¼
[Fact]
public void TreeStructure_循ç°åç
§_ææ£ç¢ºèç()
{
// 建ç«å
·æç¶åéååç
§ç樹ççµæ§
var parent = new TreeNode { Value = "Root" };
var child1 = new TreeNode { Value = "Child1", Parent = parent };
var child2 = new TreeNode { Value = "Child2", Parent = parent };
parent.Children = new[] { child1, child2 };
var actualTree = treeService.GetTree("Root");
// èç循ç°åç
§
actualTree.Should().BeEquivalentTo(parent, options =>
options.IgnoringCyclicReferences()
.WithMaxRecursionDepth(10)
);
}
3-6. é²éæ¯å°æ¨¡å¼
FluentAssertions éæä¾å¤ç¨®é²éæ¯å°æ¨¡å¼ï¼åæ æ¬ä½æé¤ï¼æé¤æéæ³è¨ãèªåçææ¬ä½ï¼ãå·¢çç©ä»¶æ¬ä½æé¤ã大éè³ææè½æä½³åæ¯å°ï¼é¸ææ§å±¬æ§æ¯å°ãæ½æ¨£é©èçç¥ï¼ã以åå´æ ¼/å¯¬é¬æåºæ§å¶ã
ð 宿´ç¨å¼ç¢¼ç¯ä¾è«åé± references/detailed-comparison-patterns.md
æ¯å°é¸é éæ¥è¡¨
| é¸é æ¹æ³ | ç¨é | é©ç¨å ´æ¯ |
|---|---|---|
Excluding(x => x.Property) |
æé¤ç¹å®å±¬æ§ | æé¤æéæ³è¨ãèªåçææ¬ä½ |
Including(x => x.Property) |
åªå å«ç¹å®å±¬æ§ | ééµå±¬æ§é©è |
IgnoringCyclicReferences() |
忽ç¥å¾ªç°åç § | 樹ççµæ§ãéåéè¯ |
WithMaxRecursionDepth(n) |
éå¶é迴深度 | 深層巢ççµæ§ |
WithStrictOrdering() |
å´æ ¼é åºæ¯å° | é£å/éåé åºéè¦æ |
WithoutStrictOrdering() |
寬é¬é åºæ¯å° | é£å/éåé åºä¸éè¦æ |
WithTracing() |
åç¨è¿½è¹¤ | é¤é¯è¤éæ¯å°å¤±æ |
å¸¸è¦æ¯å°æ¨¡å¼èè§£æ±ºæ¹æ¡
æ¨¡å¼ 1ï¼Entity Framework 坦髿¯å°
[Fact]
public void EFEntity_è³æåº«å¯¦é«_ææé¤å°èªå±¬æ§()
{
var expected = new Product { Id = 1, Name = "Laptop", Price = 999 };
var actual = dbContext.Products.Find(1);
actual.Should().BeEquivalentTo(expected, options =>
options.ExcludingMissingMembers() // æé¤ EF 追蹤屬æ§
.Excluding(p => p.CreatedAt)
.Excluding(p => p.UpdatedAt)
);
}
æ¨¡å¼ 2ï¼API Response æ¯å°
[Fact]
public void ApiResponse_JSONååºåå_æå¿½ç¥é¡å¤æ¬ä½()
{
var expected = new UserDto
{
Id = 1,
Username = "john_doe"
};
var response = await httpClient.GetAsync("/api/users/1");
var actual = await response.Content.ReadFromJsonAsync<UserDto>();
actual.Should().BeEquivalentTo(expected, options =>
options.ExcludingMissingMembers() // å¿½ç¥ API é¡å¤æ¬ä½
);
}
æ¨¡å¼ 3ï¼æ¸¬è©¦è³æå»ºæ§å¨æ¯å°
[Fact]
public void Builder_æ¸¬è©¦è³æ_æå¹é
é æçµæ§()
{
var expected = new OrderBuilder()
.WithId(1)
.WithCustomer("John Doe")
.WithItems(3)
.Build();
var actual = orderService.CreateOrder(orderRequest);
actual.Should().BeEquivalentTo(expected, options =>
options.Excluding(o => o.OrderNumber) // 系統çæ
.Excluding(o => o.CreatedAt)
);
}
é¯èª¤è¨æ¯æä½³å
æä¾ææç¾©çé¯èª¤è¨æ¯
[Fact]
public void Comparison_é¯èª¤è¨æ¯_ææ¸
æ¥èªªæå·®ç°()
{
var expected = new User { Name = "John", Age = 30 };
var actual = userService.GetUser(1);
// ä½¿ç¨ because 忏æä¾ä¸ä¸æ
actual.Should().BeEquivalentTo(expected, options =>
options.Excluding(u => u.Id)
.Because("ID æ¯ç³»çµ±èªåçæçï¼ä¸æç´å
¥æ¯å°")
);
}
ä½¿ç¨ AssertionScope é²è¡æ¹æ¬¡é©è
[Fact]
public void MultipleComparisons_æ¹æ¬¡é©è_æä¸æ¬¡é¡¯ç¤ºææå¤±æ()
{
var users = userService.GetAllUsers();
using (new AssertionScope())
{
foreach (var user in users)
{
user.Id.Should().BeGreaterThan(0);
user.Name.Should().NotBeNullOrEmpty();
user.Email.Should().MatchRegex(@"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$");
}
}
// ææå¤±ææä¸èµ·å ±åï¼èééå°ç¬¬ä¸å失æå°±åæ¢
}
èå ¶ä»æè½æ´å
æ¤æè½å¯è以䏿è½çµå使ç¨ï¼
- awesome-assertions-guide: åºç¤æ·è¨èªæ³èå¸¸ç¨ API
- autofixture-data-generation: èªåçææ¸¬è©¦è³æ
- test-data-builder-pattern: 建æ§è¤é測試ç©ä»¶
- unit-test-fundamentals: å®å 測試åºç¤è 3A 模å¼
æä½³å¯¦è¸å»ºè°
â æ¨è¦åæ³
- åªå
使ç¨å±¬æ§æé¤èéå
å«ï¼é¤éåªéé©èå°æ¸å±¬æ§ï¼å¦å使ç¨
Excludingæ´æ¸ æ¥ - 建ç«å¯éç¨çæé¤æ´å æ¹æ³ï¼é¿å 卿¯å測試éè¤æé¤é輯
- çºå¤§éè³ææ¯å°è¨å®åççç¥ï¼å¹³è¡¡æè½èé©è宿´æ§
- ä½¿ç¨ AssertionScope é²è¡æ¹æ¬¡é©èï¼ä¸æ¬¡çå°ææå¤±æåå
- æä¾ææç¾©ç because 說æï¼å¹«å©æªä¾ç¶è·è ç解測試æå
â é¿å åæ³
- é¿å é度ä¾è³´å®æ´ç©ä»¶æ¯å°ï¼èæ ®åªé©èééµå±¬æ§
- é¿å
忽ç¥å¾ªç°åç
§åé¡ï¼ä½¿ç¨
IgnoringCyclicReferences()æç¢ºèç - é¿å 卿¯å測試éè¤æé¤éè¼¯ï¼æåçºæ´å æ¹æ³
- é¿å å°å¤§éè³æå宿´æ·±åº¦æ¯å°ï¼ä½¿ç¨æ½æ¨£æééµå±¬æ§é©è
çé£æè§£
Q1: BeEquivalentTo æè½å¾æ ¢æéº¼è¾¦ï¼
A: 使ç¨ä»¥ä¸çç¥åªåï¼
- 使ç¨
Includingåªæ¯å°ééµå±¬æ§ - å°å¤§éè³ææ¡ç¨æ½æ¨£é©è
- 使ç¨
WithMaxRecursionDepthéå¶é迴深度 - èæ
®ä½¿ç¨
AssertKeyPropertiesOnlyå¿«éæ¯å°é鵿¬ä½
Q2: å¦ä½èç StackOverflowExceptionï¼
A: é常ç±å¾ªç°åç §å¼èµ·ï¼
options.IgnoringCyclicReferences()
.WithMaxRecursionDepth(10)
Q3: å¦ä½æé¤æææéç¸éæ¬ä½ï¼
A: 使ç¨è·¯å¾æ¨¡å¼å¹é ï¼
options.Excluding(ctx => ctx.Path.EndsWith("At"))
.Excluding(ctx => ctx.Path.EndsWith("Time"))
.Excluding(ctx => ctx.Path.Contains("Timestamp"))
Q4: æ¯å°å¤±æä½çä¸åºå·®ç°ï¼
A: åç¨è©³ç´°è¿½è¹¤ï¼
options.WithTracing() // ç¢çè©³ç´°çæ¯å°è¿½è¹¤è³è¨
ç¯æ¬æªæ¡åè
æ¬æè½æä¾ä»¥ä¸ç¯æ¬æªæ¡ï¼
templates/comparison-patterns.cs: å¸¸è¦æ¯å°æ¨¡å¼ç¯ä¾templates/exclusion-strategies.cs: æ¬ä½æé¤çç¥èæ´å æ¹æ³
åèè³æº
åå§æç«
æ¬æè½å §å®¹æç èªãèæ´¾è»é«å·¥ç¨å¸«ç測試修練 – 30 å¤©ææ°ãç³»åæç« ï¼
- Day 05 – AwesomeAssertions é²éæå·§èè¤éæ
墿ç¨
- éµäººè³½æç« ï¼https://ithelp.ithome.com.tw/articles/10374425
- ç¯ä¾ç¨å¼ç¢¼ï¼https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day05
宿¹æä»¶
ç¸éæè½
awesome-assertions-guide– AwesomeAssertions åºç¤èé²éç¨æ³unit-test-fundamentals– å®å 測試åºç¤