dotnet-testing-complex-object-comparison

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

複雜物件比對指南(Complex Object Comparison)

Guide to Complex Object Comparison

適用情境

Applicable Scenarios

此技能專注於 .NET 測試中的複雜物件比對場景,使用 AwesomeAssertions 的
BeEquivalentTo
API 處理各種進階比對需求。
This skill focuses on complex object comparison scenarios in .NET testing, using the
BeEquivalentTo
API from AwesomeAssertions to handle various advanced comparison requirements.

核心使用場景

Core Usage Scenarios

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

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);
}
When you need to compare complex objects with multi-level nested properties:
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)

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)
    );
}
Handle situations where there are circular references between objects:
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-6. 進階比對模式

3-6. Advanced Comparison Patterns

FluentAssertions 還提供多種進階比對模式:動態欄位排除(排除時間戳記、自動生成欄位)、巢狀物件欄位排除、大量資料效能最佳化比對(選擇性屬性比對、抽樣驗證策略)、以及嚴格/寬鬆排序控制。
📖 完整程式碼範例請參閱 references/detailed-comparison-patterns.md
FluentAssertions also provides various advanced comparison patterns: dynamic field exclusion (exclude timestamps, auto-generated fields), nested object field exclusion, performance-optimized comparison for large datasets (selective property comparison, sampling validation strategies), and strict/relaxed ordering control.
📖 For complete code examples, refer to references/detailed-comparison-patterns.md

比對選項速查表

Comparison Options Cheat Sheet

選項方法用途適用場景
Excluding(x => x.Property)
排除特定屬性排除時間戳記、自動生成欄位
Including(x => x.Property)
只包含特定屬性關鍵屬性驗證
IgnoringCyclicReferences()
忽略循環參照樹狀結構、雙向關聯
WithMaxRecursionDepth(n)
限制遞迴深度深層巢狀結構
WithStrictOrdering()
嚴格順序比對陣列/集合順序重要時
WithoutStrictOrdering()
寬鬆順序比對陣列/集合順序不重要時
WithTracing()
啟用追蹤除錯複雜比對失敗
Option MethodPurposeApplicable Scenario
Excluding(x => x.Property)
Exclude specific propertiesExclude timestamps, auto-generated fields
Including(x => x.Property)
Include only specific propertiesKey property validation
IgnoringCyclicReferences()
Ignore circular referencesTree structures, bidirectional associations
WithMaxRecursionDepth(n)
Limit recursion depthDeep nested structures
WithStrictOrdering()
Strict order comparisonWhen array/collection order matters
WithoutStrictOrdering()
Relaxed order comparisonWhen array/collection order doesn't matter
WithTracing()
Enable tracingDebug complex comparison failures

常見比對模式與解決方案

Common Comparison Patterns and Solutions

模式 1:Entity Framework 實體比對

Pattern 1: Entity Framework Entity Comparison

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)
    );
}
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 比對

Pattern 2: API Response Comparison

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 額外欄位
    );
}
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:測試資料建構器比對

Pattern 3: Test Data Builder Comparison

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 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)
    );
}

錯誤訊息最佳化

Error Message Optimization

提供有意義的錯誤訊息

Provide Meaningful Error Messages

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 是系統自動生成的,不應納入比對")
    );
}
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 進行批次驗證

Use AssertionScope for Batch Validation

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}$");
        }
    }
    // 所有失敗會一起報告,而非遇到第一個失敗就停止
}
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}$");
        }
    }
    // 所有失敗會一起報告,而非遇到第一個失敗就停止
}

與其他技能整合

Integration with Other Skills

此技能可與以下技能組合使用:
  • awesome-assertions-guide: 基礎斷言語法與常用 API
  • autofixture-data-generation: 自動生成測試資料
  • test-data-builder-pattern: 建構複雜測試物件
  • unit-test-fundamentals: 單元測試基礎與 3A 模式
This skill can be used in combination with the following skills:
  • awesome-assertions-guide: Basic assertion syntax and commonly used APIs
  • autofixture-data-generation: Automatically generate test data
  • test-data-builder-pattern: Construct complex test objects
  • unit-test-fundamentals: Unit testing basics and 3A pattern

最佳實踐建議

Best Practice Recommendations

✅ 推薦做法

✅ Recommended Practices

  1. 優先使用屬性排除而非包含:除非只需驗證少數屬性,否則使用
    Excluding
    更清楚
  2. 建立可重用的排除擴充方法:避免在每個測試重複排除邏輯
  3. 為大量資料比對設定合理策略:平衡效能與驗證完整性
  4. 使用 AssertionScope 進行批次驗證:一次看到所有失敗原因
  5. 提供有意義的 because 說明:幫助未來維護者理解測試意圖
  1. Prefer property exclusion over inclusion: Use
    Excluding
    for clarity unless you only need to validate a few properties
  2. Create reusable exclusion extension methods: Avoid repeating exclusion logic in each test
  3. Set reasonable strategies for large dataset comparison: Balance performance and validation completeness
  4. Use AssertionScope for batch validation: See all failure reasons at once
  5. Provide meaningful because explanations: Help future maintainers understand test intent

❌ 避免做法

❌ Practices to Avoid

  1. 避免過度依賴完整物件比對:考慮只驗證關鍵屬性
  2. 避免忽略循環參照問題:使用
    IgnoringCyclicReferences()
    明確處理
  3. 避免在每個測試重複排除邏輯:提取為擴充方法
  4. 避免對大量資料做完整深度比對:使用抽樣或關鍵屬性驗證
  1. Avoid over-reliance on full object comparison: Consider validating only key properties
  2. Avoid ignoring circular reference issues: Explicitly handle them with
    IgnoringCyclicReferences()
  3. Avoid repeating exclusion logic in each test: Extract it into extension methods
  4. Avoid full deep comparison for large datasets: Use sampling or key property validation

疑難排解

Troubleshooting

Q1: BeEquivalentTo 效能很慢怎麼辦?

Q1: BeEquivalentTo is very slow, what should I do?

A: 使用以下策略優化:
  • 使用
    Including
    只比對關鍵屬性
  • 對大量資料採用抽樣驗證
  • 使用
    WithMaxRecursionDepth
    限制遞迴深度
  • 考慮使用
    AssertKeyPropertiesOnly
    快速比對關鍵欄位
A: Use the following optimization strategies:
  • Use
    Including
    to compare only key properties
  • Use sampling validation for large datasets
  • Use
    WithMaxRecursionDepth
    to limit recursion depth
  • Consider using
    AssertKeyPropertiesOnly
    for quick key field comparison

Q2: 如何處理 StackOverflowException?

Q2: How to handle StackOverflowException?

A: 通常由循環參照引起:
csharp
options.IgnoringCyclicReferences()
       .WithMaxRecursionDepth(10)
A: Usually caused by circular references:
csharp
options.IgnoringCyclicReferences()
       .WithMaxRecursionDepth(10)

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

Q3: How to exclude all time-related fields?

A: 使用路徑模式匹配:
csharp
options.Excluding(ctx => ctx.Path.EndsWith("At"))
       .Excluding(ctx => ctx.Path.EndsWith("Time"))
       .Excluding(ctx => ctx.Path.Contains("Timestamp"))
A: Use path pattern matching:
csharp
options.Excluding(ctx => ctx.Path.EndsWith("At"))
       .Excluding(ctx => ctx.Path.EndsWith("Time"))
       .Excluding(ctx => ctx.Path.Contains("Timestamp"))

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

Q4: Comparison fails but no differences are visible?

A: 啟用詳細追蹤:
csharp
options.WithTracing()  // 產生詳細的比對追蹤資訊
A: Enable detailed tracing:
csharp
options.WithTracing()  // 產生詳細的比對追蹤資訊

範本檔案參考

Template File References

本技能提供以下範本檔案:
  • templates/comparison-patterns.cs
    : 常見比對模式範例
  • templates/exclusion-strategies.cs
    : 欄位排除策略與擴充方法
This skill provides the following template files:
  • templates/comparison-patterns.cs
    : Common comparison pattern examples
  • templates/exclusion-strategies.cs
    : Field exclusion strategies and extension methods

參考資源

Reference Resources

原始文章

Original Articles

本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
This skill is extracted from the "Old-School Software Engineer's Testing Practice - 30-Day Challenge" series of articles:

官方文件

Official Documentation

相關技能

Related Skills

  • awesome-assertions-guide
    - AwesomeAssertions 基礎與進階用法
  • unit-test-fundamentals
    - 單元測試基礎
  • awesome-assertions-guide
    - Basic and advanced usage of AwesomeAssertions
  • unit-test-fundamentals
    - Unit testing basics