当类型为 C# 9 记录时,FluentAssertions Should().BeEquivalentTo() 失败,看似将 objects 视为字符串

FluentAssertions Should().BeEquivalentTo() fails in trivial case when types are C# 9 records, seemingly treating objects as strings

我最近开始使用 FluentAssertions,它应该具有强大的 object 图表比较功能。

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

谁能给我解释一下,我才两岁,怎么了?

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

失败并显示此消息:

Message:

Expected result.Address to be 4 Some street, 12345 Toronto, Canada, but found AddressDto { Country = Canada, ZipCode = 12345, City = Toronto, Street = 4 Some street }.

With configuration:

  • Use declared types and members
  • Compare enums by value
  • Match member by name (or throw)
  • Without automatic conversion.
  • Be strict about the order of items in byte arrays

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

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


故事寓意:

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

但是,在这种情况下,recordEquals() 的默认覆盖实现实际上仅在两种类型相同时才有效,因此失败,因此 FluentAssertions 报告 BeEquivalentTo().

并且,在失败消息中,FluentAssertions 通过 ToString() 将 objects 转换为字符串来混淆地报告问题。这是因为记录有 'value semantics',所以它会这样对待它们。有一个 open issue about this on GitHub.

我确认将 record 更改为 class 不会出现问题。

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

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


参考文献:

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

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, MinLength(1), MaxLength(100)]
    public string City { get; init; }

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

    [Required, MinLength(1), 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}";
}

这里是 Dto 等价物:

public record PartnerDetailsDto : IMapFrom<Partner>
{
    public int Id { get; init; }
    public string Name { get; init; }
    public DateTime CreatedAt { get; init; }
    public DateTime? LastModifiedAt { get; init; }

    public AddressDto Address { get; init; }

    public void Mapping(Profile profile)
    {
        profile.CreateMap<Partner, PartnerDetailsDto>();
        profile.CreateMap<Address, AddressDto>();
    }

    public record AddressDto
    {
        public string Country { get; init; }
        public string ZipCode { get; init; }
        public string City { get; init; }
        public string Street { get; init; }
    }
}

您尝试过使用 options.ComparingByMembers<Address>() 吗?

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

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

To determine whether Fluent Assertions should recurs into an object’s properties or fields, it needs to understand what types have value semantics and what types should be treated as reference types. The default behavior is to treat every type that overrides Object.Equals as an object that was designed to have value semantics

您的两个记录都会覆盖 Equals,但它们的 Equals 方法只有在另一个对象属于同一类型时才 return 为真。所以我认为 Should().BeEquivalentTo 看到你的对象实现了它们自己的相等性,调用(大概)AddressDto.Equals 其中 return 是假的,然后报告失败。

它使用两个记录的 ToString() 版本报告失败,return { 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>());