You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 18:57:46