dotnet-testing-complex-object-comparison
SKILL.md
Complex Object Comparison Skill
技能說明
此技能專注於 .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. 動態欄位排除 (Dynamic Field Exclusion)
3.1 排除時間戳記與自動生成欄位
[Fact]
public void Entity_排除自動欄位_應驗證業務欄位()
{
var originalEntity = new UserEntity
{
Id = 1,
Name = "John Doe",
Email = "john@example.com",
CreatedAt = DateTime.Now.AddDays(-1),
UpdatedAt = DateTime.Now.AddDays(-1),
Version = 1
};
var updatedEntity = userService.UpdateUser(1, new UpdateUserRequest
{
Name = "John Doe",
Email = "john@example.com"
});
// 排除自動更新的欄位
updatedEntity.Should().BeEquivalentTo(originalEntity, options =>
options.Excluding(e => e.UpdatedAt)
.Excluding(e => e.Version)
.Excluding(e => e.LastModifiedBy)
);
// 單獨驗證動態欄位
updatedEntity.UpdatedAt.Should().BeAfter(originalEntity.UpdatedAt);
updatedEntity.Version.Should().Be(originalEntity.Version + 1);
}
3.2 使用智慧型排除擴充方法
// 定義可重複使用的排除策略
public static class SmartExclusionExtensions
{
public static EquivalencyOptions<T> ExcludingAutoGeneratedFields<T>(
this EquivalencyOptions<T> options)
{
return options
.Excluding(ctx => ctx.Path.EndsWith("Id") &&
ctx.SelectedMemberInfo.Name.StartsWith("Generated"))
.Excluding(ctx => ctx.Path.EndsWith("At"))
.Excluding(ctx => ctx.Path.EndsWith("Time"))
.Excluding(ctx => ctx.Path.Contains("Version"))
.Excluding(ctx => ctx.Path.Contains("Timestamp"));
}
public static EquivalencyOptions<T> ExcludingAuditFields<T>(
this EquivalencyOptions<T> options)
{
return options
.Excluding(ctx => ctx.Path.Contains("CreatedBy"))
.Excluding(ctx => ctx.Path.Contains("CreatedAt"))
.Excluding(ctx => ctx.Path.Contains("ModifiedBy"))
.Excluding(ctx => ctx.Path.Contains("ModifiedAt"));
}
}
[Fact]
public void Entity_使用智慧排除_應簡化測試()
{
var user = userService.CreateUser("test@example.com");
var retrievedUser = userService.GetUser(user.Id);
// 使用智慧排除擴充方法
retrievedUser.Should().BeEquivalentTo(user, options =>
options.ExcludingAutoGeneratedFields()
.ExcludingAuditFields()
);
}
4. 巢狀物件欄位排除 (Nested Object Exclusion)
[Fact]
public void ComplexEntity_排除巢狀時間戳記_應正常運作()
{
var order = new Order
{
Id = 1,
CustomerName = "John Doe",
CreatedAt = DateTime.Now,
Items = new[]
{
new OrderItem
{
Id = 1,
ProductName = "Laptop",
AddedAt = DateTime.Now
}
},
AuditInfo = new AuditInfo
{
CreatedBy = "system",
CreatedAt = DateTime.Now
}
};
var retrievedOrder = orderService.GetOrder(1);
// 使用路徑模式排除所有時間戳記
retrievedOrder.Should().BeEquivalentTo(order, options =>
options.Excluding(ctx => ctx.Path.EndsWith("At"))
.Excluding(ctx => ctx.Path.EndsWith("Time"))
);
}
5. 大量資料比對效能最佳化
5.1 選擇性屬性比對
[Fact]
public void LargeDataSet_選擇性比對_應高效執行()
{
var largeDataset = Enumerable.Range(1, 100000)
.Select(i => new DataRecord
{
Id = i,
Value = $"Record_{i}",
Timestamp = DateTime.Now
})
.ToList();
var processed = dataProcessor.Process(largeDataset);
// 只比對關鍵屬性,忽略非關鍵欄位
processed.Should().BeEquivalentTo(largeDataset, options =>
options.Including(x => x.Id)
.Including(x => x.Value)
.Excluding(x => x.Timestamp)
);
}
5.2 抽樣驗證策略
[Fact]
public void LargeCollection_抽樣驗證_應平衡效能與準確性()
{
var largeDataset = GenerateLargeDataSet(100000);
var processed = service.ProcessLargeDataset(largeDataset);
// 驗證數量
processed.Should().HaveCount(largeDataset.Count);
// 抽樣驗證
var sampleSize = Math.Min(1000, processed.Count / 10);
var sampleIndices = Enumerable.Range(0, sampleSize)
.Select(i => Random.Shared.Next(processed.Count))
.Distinct()
.ToList();
foreach (var index in sampleIndices)
{
processed[index].Should().BeEquivalentTo(largeDataset[index], options =>
options.ExcludingAutoGeneratedFields()
);
}
// 統計驗證
processed.Count(r => r.IsProcessed).Should().Be(processed.Count);
}
5.3 關鍵屬性快速比對
public static class PerformanceOptimizedAssertions
{
public static void AssertKeyPropertiesOnly<T>(
T actual,
T expected,
params Expression<Func<T, object>>[] keySelectors)
{
foreach (var selector in keySelectors)
{
var actualValue = selector.Compile()(actual);
var expectedValue = selector.Compile()(expected);
actualValue.Should().Be(expectedValue,
$"關鍵屬性 {selector} 應該相符");
}
}
}
[Fact]
public void Order_關鍵屬性驗證_應快速完成()
{
var expected = new Order
{
Id = 1,
CustomerName = "John",
TotalAmount = 999.99m,
CreatedAt = DateTime.Now
};
var actual = orderService.GetOrder(1);
// 只比對關鍵屬性,忽略時間戳記
PerformanceOptimizedAssertions.AssertKeyPropertiesOnly(
actual,
expected,
o => o.Id,
o => o.CustomerName,
o => o.TotalAmount
);
}
6. 嚴格順序與寬鬆比對
[Fact]
public void Collection_順序控制_應符合需求()
{
var expected = new[] { "A", "B", "C" };
var actualStrict = service.GetOrderedList();
var actualLoose = service.GetUnorderedList();
// 嚴格順序比對
actualStrict.Should().BeEquivalentTo(expected, options =>
options.WithStrictOrdering()
);
// 寬鬆比對(不考慮順序)
actualLoose.Should().BeEquivalentTo(expected, options =>
options.WithoutStrictOrdering()
);
}
比對選項速查表
| 選項方法 | 用途 | 適用場景 |
|---|---|---|
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 進階技巧與複雜情境應用
官方文件
相關技能
awesome-assertions-guide- AwesomeAssertions 基礎與進階用法unit-test-fundamentals- 單元測試基礎
Weekly Installs
6
Repository
kevintsengtw/dotnet-testing-agent-skillsInstalled on
claude-code5
antigravity4
gemini-cli4
windsurf3
opencode3
codex3