如何用Fluent Assertions比较通过ID关联的对象图?
用Fluent Assertions的
BeEquivalentTo验证基于ID关联的对象图 当然可以!Fluent Assertions的BeEquivalentTo之所以是它的核心卖点之一,就是因为它高度可配置,完全能处理你这种基于ID属性而非对象引用的对象图验证需求——而且完全不需要手动构建对象引用,避免引入额外bug。
核心思路是利用BeEquivalentTo的Using+When组合自定义属性级别的断言逻辑,再配合AfterAllAssertions做全局结构验证,就能精准匹配你描述的三种场景。下面针对每个示例给出具体实现:
示例A:验证NextId与对应对象的Id关联
场景说明
需要验证集合中存在两个对象:一个Name为"Foo"且NextId指向另一个对象的Id,另一个Name为"Bar",不关心Id的具体数值和对象顺序。
实现代码
假设我们定义如下实体类:
public class ItemA { public int Id { get; set; } public string Name { get; set; } public int? NextId { get; set; } }
断言逻辑如下:
var actualItems = // 从文件/数据库加载的目标集合 var expectedStructure = new[] { new ItemA { Name = "Foo", NextId = 0 }, // 0是占位符,仅用于标识属性存在 new ItemA { Name = "Bar" } }; actualItems.Should().BeEquivalentTo(expectedStructure, options => options // 自定义NextId的验证逻辑:确保该值对应集合中某个对象的Id .Using<int>(ctx => { var targetId = ctx.Subject; actualItems.Any(item => item.Id == targetId) .Should().BeTrue($"NextId {targetId} 必须指向集合中的一个有效对象Id"); }) .When(info => info.Path.EndsWith(nameof(ItemA.NextId)) && info.Subject != null) // 忽略Id的具体数值,我们只关心关联关系而非Id本身 .Using<int>(_ => { }) .When(info => info.Path.EndsWith(nameof(ItemA.Id))) );
匹配/不匹配验证
- 匹配案例:比如
[{ "Id": 5, "Name": "Foo", "NextId": -8 }, { "Id": -8, "Name": "Bar" }]会通过——NextId=-8对应另一个对象的Id=-8,且Name符合预期。 - 不匹配案例:
[{ "Id": 3, "Name": "Bar", "NextId": 6 }, { "Id": 6, "Name": "Foo" }]会失败——虽然NextId关联正确,但Name的对应关系不符合预期,BeEquivalentTo会检测到这个属性不匹配。
示例B:验证双向Key关联与空LinkedTo
场景说明
需要验证集合中有三个对象:两个对象的LinkedTo互相指向对方的Key,第三个对象的LinkedTo为空数组,不关心Key的具体数值。
实现代码
假设实体类定义:
public class ItemB { public int Key { get; set; } public List<int> LinkedTo { get; set; } = new(); }
断言逻辑:
var actualItems = // 加载的目标集合 var expectedStructure = new[] { new ItemB { LinkedTo = new List<int> { 0 } }, // 占位符标识关联存在 new ItemB { LinkedTo = new() }, new ItemB { LinkedTo = new List<int> { 0 } } }; actualItems.Should().BeEquivalentTo(expectedStructure, options => options // 验证LinkedTo中的每个Key都存在于集合中 .Using<int>(ctx => { var linkedKey = ctx.Subject; actualItems.Any(item => item.Key == linkedKey) .Should().BeTrue($"LinkedTo中的Key {linkedKey} 必须存在于集合中"); }) .When(info => info.Path.Contains(nameof(ItemB.LinkedTo))) // 忽略Key的具体数值 .Using<int>(_ => { }) .When(info => info.Path.EndsWith(nameof(ItemB.Key))) // 全局验证:确保存在且仅存在一对双向关联,以及一个空LinkedTo的对象 .AfterAllAssertions(_ => { // 统计双向关联的对 var bidirectionalPairs = actualItems .SelectMany(item => item.LinkedTo.Select(k => (Source: item.Key, Target: k))) .Where(pair => actualItems.Any(item => item.Key == pair.Target && item.LinkedTo.Contains(pair.Source))) .Distinct(); bidirectionalPairs.Count().Should().Be(1, "必须存在且仅存在一对互相引用的对象"); actualItems.Count(item => !item.LinkedTo.Any()).Should().Be(1, "必须有且仅有一个对象的LinkedTo为空"); }) );
匹配/不匹配验证
- 匹配案例:
[{ "Key": 20, "LinkedTo": [7] }, { "Key": 8, "LinkedTo": [] }, { "Key": 7, "LinkedTo": [20] }]会通过——满足双向关联和空LinkedTo的要求。 - 不匹配案例:
[{ "Key": 5, "LinkedTo": [10] }, { "Key": 10, "LinkedTo": [] }, { "Key": 3, "LinkedTo": [5] }]会失败——没有双向关联的对,AfterAllAssertions中的检查会触发失败。
示例C:验证多个对象的Owner指向同一个Id
场景说明
需要验证集合中有四个对象:三个对象的Owner值指向第四个对象的Id,不关心Id的具体数值。
实现代码
假设实体类定义:
public class ItemC { public int Id { get; set; } public int? Owner { get; set; } }
断言逻辑:
var actualItems = // 加载的目标集合 var expectedStructure = new[] { new ItemC { Owner = 0 }, // 占位符标识Owner属性存在 new ItemC { Owner = 0 }, new ItemC { Owner = 0 }, new ItemC { Owner = null } }; actualItems.Should().BeEquivalentTo(expectedStructure, options => options // 验证Owner值对应集合中的一个有效Id,且所有非空Owner指向同一个Id .Using<int>(ctx => { var ownerId = ctx.Subject; var ownerItem = actualItems.FirstOrDefault(item => item.Id == ownerId); ownerItem.Should().NotBeNull($"Owner {ownerId} 必须指向集合中的一个有效对象Id"); // 确保所有非空Owner都指向同一个Id var allOwnerIds = actualItems.Where(item => item.Owner.HasValue).Select(item => item.Owner.Value); allOwnerIds.Distinct().Count().Should().Be(1, "所有非空Owner必须指向同一个对象的Id"); }) .When(info => info.Path.EndsWith(nameof(ItemC.Owner)) && info.Subject != null) // 忽略Id的具体数值 .Using<int>(_ => { }) .When(info => info.Path.EndsWith(nameof(ItemC.Id))) );
匹配/不匹配验证
- 匹配案例:
[{ "Id": 10, "Owner": 20 }, { "Id": 30, "Owner": 20 }, { "Id": 20 }, { "Id": 40, "Owner": 20 }]会通过——所有非空Owner都指向Id=20的对象。 - 不匹配案例:
[{ "Id": 8, "Owner": 2 }, { "Id": 12, "Owner": 8 }, { "Id": 54, "Owner": 2 }, { "Id": 2 }]会失败——存在两个不同的Owner值(2和8),全局检查会触发失败。
这些实现完全不需要手动构建对象引用,所有验证都基于属性值的关联关系,既满足你的需求,又避免了手动引用可能带来的错误。
内容的提问来源于stack exchange,提问作者F-H
相关产品推荐
相关产品推荐

