$npx -y skills add kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-complex-object-comparison處理複雜物件比對與深層驗證的專門技能。當需要比對深層物件、排除特定屬性、處理循環參照、驗證 DTO/Entity 時使用。涵蓋 BeEquivalentTo、Excluding、Including、自訂比對規則等。 Make sure to use this skill whenever the user mentions deep object comparison, BeEquivalentTo, DTO comparison, Excluding properties, or complex object validation in tests,
| 1 | # 複雜物件比對指南(Complex Object Comparison) |
| 2 | |
| 3 | ## 核心使用場景 |
| 4 | |
| 5 | ### 1. 深層物件結構比對 (Object Graph Comparison) |
| 6 | |
| 7 | 當需要比對包含多層巢狀屬性的複雜物件時: |
| 8 | |
| 9 | ```csharp |
| 10 | [Fact] |
| 11 | public void ComplexObject_深層結構比對_應完全相符() |
| 12 | { |
| 13 | var expected = new Order |
| 14 | { |
| 15 | Id = 1, |
| 16 | Customer = new Customer |
| 17 | { |
| 18 | Name = "John Doe", |
| 19 | Address = new Address |
| 20 | { |
| 21 | Street = "123 Main St", |
| 22 | City = "Seattle", |
| 23 | ZipCode = "98101" |
| 24 | } |
| 25 | }, |
| 26 | Items = new[] |
| 27 | { |
| 28 | new OrderItem { ProductName = "Laptop", Quantity = 1, Price = 999.99m }, |
| 29 | new OrderItem { ProductName = "Mouse", Quantity = 2, Price = 29.99m } |
| 30 | } |
| 31 | }; |
| 32 | |
| 33 | var actual = orderService.GetOrder(1); |
| 34 | |
| 35 | // 深層物件比對 |
| 36 | actual.Should().BeEquivalentTo(expected); |
| 37 | } |
| 38 | ``` |
| 39 | |
| 40 | ### 2. 循環參照處理 (Circular Reference Handling) |
| 41 | |
| 42 | 處理物件之間存在循環參照的情況: |
| 43 | |
| 44 | ```csharp |
| 45 | [Fact] |
| 46 | public void TreeStructure_循環參照_應正確處理() |
| 47 | { |
| 48 | // 建立具有父子雙向參照的樹狀結構 |
| 49 | var parent = new TreeNode { Value = "Root" }; |
| 50 | var child1 = new TreeNode { Value = "Child1", Parent = parent }; |
| 51 | var child2 = new TreeNode { Value = "Child2", Parent = parent }; |
| 52 | parent.Children = new[] { child1, child2 }; |
| 53 | |
| 54 | var actualTree = treeService.GetTree("Root"); |
| 55 | |
| 56 | // 處理循環參照 |
| 57 | actualTree.Should().BeEquivalentTo(parent, options => |
| 58 | options.IgnoringCyclicReferences() |
| 59 | .WithMaxRecursionDepth(10) |
| 60 | ); |
| 61 | } |
| 62 | ``` |
| 63 | |
| 64 | ### 3-6. 進階比對模式 |
| 65 | |
| 66 | AwesomeAssertions 還提供多種進階比對模式:動態欄位排除(排除時間戳記、自動生成欄位)、巢狀物件欄位排除、大量資料效能最佳化比對(選擇性屬性比對、抽樣驗證策略)、以及嚴格/寬鬆排序控制。 |
| 67 | |
| 68 | > 完整程式碼範例請參閱 [references/detailed-comparison-patterns.md](references/detailed-comparison-patterns.md) |
| 69 | |
| 70 | ## 比對選項速查表 |
| 71 | |
| 72 | | 選項方法 | 用途 | 適用場景 | |
| 73 | | ---------------------------- | -------------- | -------------------------- | |
| 74 | | `Excluding(x => x.Property)` | 排除特定屬性 | 排除時間戳記、自動生成欄位 | |
| 75 | | `Including(x => x.Property)` | 只包含特定屬性 | 關鍵屬性驗證 | |
| 76 | | `IgnoringCyclicReferences()` | 忽略循環參照 | 樹狀結構、雙向關聯 | |
| 77 | | `WithMaxRecursionDepth(n)` | 限制遞迴深度 | 深層巢狀結構 | |
| 78 | | `WithStrictOrdering()` | 嚴格順序比對 | 陣列/集合順序重要時 | |
| 79 | | `WithoutStrictOrdering()` | 寬鬆順序比對 | 陣列/集合順序不重要時 | |
| 80 | | `WithTracing()` | 啟用追蹤 | 除錯複雜比對失敗 | |
| 81 | |
| 82 | ## 常見比對模式與解決方案 |
| 83 | |
| 84 | ### 模式 1:Entity Framework 實體比對 |
| 85 | |
| 86 | ```csharp |
| 87 | [Fact] |
| 88 | public void EFEntity_資料庫實體_應排除導航屬性() |
| 89 | { |
| 90 | var expected = new Product { Id = 1, Name = "Laptop", Price = 999 }; |
| 91 | var actual = dbContext.Products.Find(1); |
| 92 | |
| 93 | actual.Should().BeEquivalentTo(expected, options => |
| 94 | options.ExcludingMissingMembers() // 排除 EF 追蹤屬性 |
| 95 | .Excluding(p => p.CreatedAt) |
| 96 | .Excluding(p => p.UpdatedAt) |
| 97 | ); |
| 98 | } |
| 99 | ``` |
| 100 | |
| 101 | ### 模式 2:API Response 比對 |
| 102 | |
| 103 | ```csharp |
| 104 | [Fact] |
| 105 | public void ApiResponse_JSON反序列化_應忽略額外欄位() |
| 106 | { |
| 107 | var expected = new UserDto |
| 108 | { |
| 109 | Id = 1, |
| 110 | Username = "john_doe" |
| 111 | }; |
| 112 | |
| 113 | var response = await httpClient.GetAsync("/api/users/1"); |
| 114 | var actual = await response.Content.ReadFromJsonAsync<UserDto>(); |
| 115 | |
| 116 | actual.Should().BeEquivalentTo(expected, options => |
| 117 | options.ExcludingMissingMembers() // 忽略 API 額外欄位 |
| 118 | ); |
| 119 | } |
| 120 | ``` |
| 121 | |
| 122 | ### 模式 3:測試資料建構器比對 |
| 123 | |
| 124 | ```csharp |
| 125 | [Fact] |
| 126 | public void Builder_測試資料_應匹配預期結構() |
| 127 | { |
| 128 | var expected = new OrderBuilder() |
| 129 | .WithId(1) |
| 130 | .WithCustomer("John Doe") |
| 131 | .WithItems(3) |
| 132 | .Build(); |
| 133 | |
| 134 | var actual = orderService.CreateOrder(orderRequest); |
| 135 | |
| 136 | actual.Should().BeEquivalentTo(expected, options => |
| 137 | options.Excluding(o => o.OrderNumber) // 系統生成 |
| 138 | .Excluding(o => o.CreatedAt) |
| 139 | ); |
| 140 | } |
| 141 | ``` |
| 142 | |
| 143 | ## 錯誤訊息最佳化 |
| 144 | |
| 145 | ### 提供有意義的錯誤訊息 |
| 146 | |
| 147 | ```csharp |
| 148 | [Fact] |
| 149 | public void Comparison_錯誤訊息_應清楚說明差異() |
| 150 | { |
| 151 | var expected = new User { Name = "John", Age = 30 }; |
| 152 | var actual = userService.GetUser(1); |
| 153 | |
| 154 | // 使用 because 參數提供上下文 |
| 155 | actual.Should().BeEquivalentTo(expected, options => |
| 156 | options.Excluding(u => u.Id) |
| 157 | .Because("ID 是系統自動生成的,不應納入比對") |
| 158 | ); |
| 159 | } |
| 160 | ``` |
| 161 | |
| 162 | ### 使用 AssertionScope 進行批次驗證 |
| 163 | |
| 164 | ```csharp |
| 165 | [Fact] |
| 166 | public void MultipleComparisons_批次驗證_應一次顯示所有失敗() |
| 167 | { |
| 168 | var users = userService.GetAllUsers(); |
| 169 | |
| 170 | using (new AssertionScope()) |
| 171 | { |
| 172 | foreach (var user in users) |
| 173 | { |
| 174 | user.Id.Should().BeGreaterThan(0); |
| 175 | user.Name.Should().NotBeNullOrEmpty(); |
| 176 | user.Email.Should().MatchRegex(@"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"); |
| 177 | } |
| 178 | } |
| 179 | // 所有失敗會一起報告, |