---
name: dotnet-testing-complex-object-comparison
description: |
  處理複雜物件比對與深層驗證的專門技能。當需要比對深層物件、排除特定屬性、處理循環參照、驗證 DTO/Entity 時使用。涵蓋 BeEquivalentTo、Excluding、Including、自訂比對規則等。
  Keywords: object comparison, 物件比對, deep comparison, 深層比對, BeEquivalentTo, DTO 比對, Entity 驗證, 排除屬性, 循環參照, Excluding, Including, ExcludingNestedObjects, RespectingRuntimeTypes, WithStrictOrdering, 忽略時間戳記, exclude timestamp
license: MIT
metadata:
  author: Kevin Tseng
  version: "1.0.0"
  tags: ".NET, testing, object comparison, BeEquivalentTo, AwesomeAssertions"
---

# Complex Object Comparison Skill

## 技能說明

此技能專注於 .NET 測試中的複雜物件比對場景，使用 AwesomeAssertions 的 `BeEquivalentTo` API 處理各種進階比對需求。

## 核心使用場景

### 1. 深層物件結構比對 (Object Graph Comparison)

當需要比對包含多層巢狀屬性的複雜物件時：

```csharp
[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)

處理物件之間存在循環參照的情況：

```csharp
[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 排除時間戳記與自動生成欄位

```csharp
[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 使用智慧型排除擴充方法

```csharp
// 定義可重複使用的排除策略
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)

```csharp
[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 選擇性屬性比對

```csharp
[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 抽樣驗證策略

```csharp
[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 關鍵屬性快速比對

```csharp
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. 嚴格順序與寬鬆比對

```csharp
[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 實體比對

```csharp
[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 比對

```csharp
[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：測試資料建構器比對

```csharp
[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)
    );
}
```

## 錯誤訊息最佳化

### 提供有意義的錯誤訊息

```csharp
[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 進行批次驗證

```csharp
[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 模式

## 最佳實踐建議

### ✅ 推薦做法

1. **優先使用屬性排除而非包含**：除非只需驗證少數屬性，否則使用 `Excluding` 更清楚
2. **建立可重用的排除擴充方法**：避免在每個測試重複排除邏輯
3. **為大量資料比對設定合理策略**：平衡效能與驗證完整性
4. **使用 AssertionScope 進行批次驗證**：一次看到所有失敗原因
5. **提供有意義的 because 說明**：幫助未來維護者理解測試意圖

### ❌ 避免做法

1. **避免過度依賴完整物件比對**：考慮只驗證關鍵屬性
2. **避免忽略循環參照問題**：使用 `IgnoringCyclicReferences()` 明確處理
3. **避免在每個測試重複排除邏輯**：提取為擴充方法
4. **避免對大量資料做完整深度比對**：使用抽樣或關鍵屬性驗證

## 疑難排解

### Q1: BeEquivalentTo 效能很慢怎麼辦？

**A:** 使用以下策略優化：

- 使用 `Including` 只比對關鍵屬性
- 對大量資料採用抽樣驗證
- 使用 `WithMaxRecursionDepth` 限制遞迴深度
- 考慮使用 `AssertKeyPropertiesOnly` 快速比對關鍵欄位

### Q2: 如何處理 StackOverflowException？

**A:** 通常由循環參照引起：

```csharp
options.IgnoringCyclicReferences()
       .WithMaxRecursionDepth(10)
```

### Q3: 如何排除所有時間相關欄位？

**A:** 使用路徑模式匹配：

```csharp
options.Excluding(ctx => ctx.Path.EndsWith("At"))
       .Excluding(ctx => ctx.Path.EndsWith("Time"))
       .Excluding(ctx => ctx.Path.Contains("Timestamp"))
```

### Q4: 比對失敗但看不出差異？

**A:** 啟用詳細追蹤：

```csharp
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

### 官方文件

- [AwesomeAssertions GitHub](https://github.com/AwesomeAssertions/AwesomeAssertions)
- [AwesomeAssertions Documentation](https://awesomeassertions.org/)

### 相關技能

- `awesome-assertions-guide` - AwesomeAssertions 基礎與進階用法
- `unit-test-fundamentals` - 單元測試基礎
