Claude-skill-registry dotnet-testing-complex-object-comparison
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/dotnet-testing-complex-object-comparison" ~/.claude/skills/majiayu000-claude-skill-registry-dotnet-testing-complex-object-comparison && rm -rf "$T"
manifest:
skills/data/dotnet-testing-complex-object-comparison/SKILL.mdsource content
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() ); }
比對選項速查表
| 選項方法 | 用途 | 適用場景 |
|---|---|---|
| 排除特定屬性 | 排除時間戳記、自動生成欄位 |
| 只包含特定屬性 | 關鍵屬性驗證 |
| 忽略循環參照 | 樹狀結構、雙向關聯 |
| 限制遞迴深度 | 深層巢狀結構 |
| 嚴格順序比對 | 陣列/集合順序重要時 |
| 寬鬆順序比對 | 陣列/集合順序不重要時 |
| 啟用追蹤 | 除錯複雜比對失敗 |
常見比對模式與解決方案
模式 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 進階技巧與複雜情境應用
官方文件
相關技能
- AwesomeAssertions 基礎與進階用法awesome-assertions-guide
- 單元測試基礎unit-test-fundamentals