当类型是 C# 9 记录时,FluentAssertions Should().BeEquivalentTo() 在微不足道的情况下失败,似乎将对象视为字符串

问题描述

我最近开始使用 FluentAssertions,它应该有这个强大的对象图比较功能

我正在尝试做可以想象到的最简单的事情:将 Address 对象的属性AddressDto 对象的属性进行比较。它们都包含 4 个简单的字符串属性:Country、City、Street 和 ZipCode(它不是生产系统)。

有人可以向我解释一下,就像我两岁一样,出了什么问题?

partnerDto.Address.Should().BeEquivalentTo(partner.Address)

它失败并显示此消息:

留言:

预期结果。地址为 4 Some street,12345 Toronto,Canada,但发现 AddressDto { Country = Canada,ZipCode = 12345,City = Toronto,Street = 4 Some street }。

有配置:

  • 使用声明的类型和成员
  • 按值比较枚举
  • 名称匹配成员(或抛出)
  • 没有自动转换。
  • 严格控制字节数组中项目的顺序

它似乎试图将 Address 对象视为字符串(因为它覆盖了 ToString()?)。我尝试使用 options.ComparingByMembers<AddressDto>() 选项,但似乎没什么区别。

(AddressDto一个 record 顺便说一句,不是 class,因为我正在用这个项目测试新的 .Net 5 功能;但它可能没有区别.)


故事的寓意:

使用 record 而不是 class 会触发 FluentAssertions,因为记录会在后台自动覆盖 Equals(),并且 FluentAssertions 假定它应该使用 Equals() 而不是属性比较,因为覆盖的 Equals() 可能是为了提供所需的比较。

但是,在这种情况下,Equals()record认覆盖实现实际上仅在两种类型相同时才有效,因此它失败,因此 FluentAssertions 在 {{1 }}。

而且,在失败消息 FluentAssertions 中,通过 ToString() 将对象转换为字符串,混淆地报告了这个问题。这是因为记录具有“值语义”,因此它如此对待它们。有一个 open issue about this on GitHub

我确认将 BeEquivalentTo() 更改为 record 不会出现问题。

(我个人认为 FluentAssertions 在 class 上时应该忽略 Equals() 覆盖并且这两种类型不同,因为这种行为可以说不是人们所期望的。当前的问题,在发布时间,属于 FluentAssertions 版本 5.10.3。)

我编辑了我的问题标题以更好地代表问题的实际情况,因此它可能对人们更有用。


参考:

正如人们所问,这里是域实体的定义(为了简洁起见,必须删除一些方法,因为我正在做 DDD,但它们肯定与问题无关):

record

这里是 Dto 的等价物:

public class Partner : MyEntity
{
    [required]
    [StringLength(PartnerInvariants.NameMaxLength)]
    public string Name { get; private set; }

    [required]
    public Address Address { get; private set; }

    public virtual IReadOnlyCollection<Transaction> Transactions => _transactions.AsReadOnly();
    private List<Transaction> _transactions = new List<Transaction>();

    private Partner()
    { }

    public Partner(string name,Address address)
    {
        UpdateName(name);
        UpdateAddress(address);
    }

    ...

    public void UpdateName(string value)
    {
        ...
    }

    public void UpdateAddress(Address address)
    {
        ...
    }

    ...
}

public record Address
{
    [required,MinLength(1),MaxLength(100)]
    public string Street { get; init; }

    [required,MaxLength(100)]
    public string City { get; init; }

    // As I mentioned,it's not a production system :)
    [required,MaxLength(100)]
    public string Country { get; init; }

    [required,MaxLength(100)]
    public string ZipCode { get; init; }

    private Address() { }

    public Address(string street,string city,string country,string zipcode)
        => (Street,City,Country,ZipCode) = (street,city,country,zipcode);

    public override string ToString()
        => $"{Street},{ZipCode} {City},{Country}";
}

解决方法

您是否尝试过使用 options.ComparingByMembers<Address>()

尝试将您的测试更改为:partnerDto.Address.Should().BeEquivalentTo(partner.Address,o => o.ComparingByMembers<Address>());

,

我认为 the docs 的重要部分是:

要确定 Fluent Assertions 是否应该递归到对象的属性或字段中,它需要了解哪些类型具有值语义以及哪些类型应该被视为引用类型。默认行为是将覆盖 Object.Equals 的每个类型视为具有值语义的对象

您的两个记录都覆盖了 Equals,但它们的 Equals 方法仅在另一个对象是相同类型时才返回 true。所以我认为 Should().BeEquivalentTo 是看到您的对象实现了它们自己的相等性,调用(大概)返回 false 的 AddressDto.Equals,然后报告失败。

它使用两条记录的 ToString() 版本报告失败,返回 { Country = Canada,ZipCode = 12345,City = Toronto,Street = 4 Some street }(对于没有覆盖 ToString 的记录)和 4 Some street,12345 Toronto,Canada,(对于具有覆盖 ToString 的对象) ).

正如文档所说,您应该能够使用 ComparingByMembers 来覆盖它:

partnerDto.Address.Should().BeEquivalentTo(partner.Address,options => options.ComparingByMembers<Address>());

或全局:

AssertionOptions.AssertEquivalencyUsing(options => options
    .ComparingByMembers<Address>());