FluentAssertions中的Should().BeEquivalentTo()在C#9记录类型相等的情况下失败,似乎将对象视为字符串。

10

最近我开始使用FluentAssertions, 它应该具备强大的对象图比较功能。

我想要做的事情非常简单:比较一个 Address 对象和一个 AddressDto 对象的属性。它们都包含四个简单的字符串属性: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() 可能就是为了提供所需的比较。

但是,在这种情况下,record 中默认的重写实现仅在两个类型相同时才起作用,因此失败,因此 FluentAssertions 报告了 BeEquivalentTo() 的错误。

而且,在失败消息中,FluentAssertions 通过 ToString() 将对象混淆报告问题。 这是因为记录具有“值语义”,因此将它们视为这样处理。 关于此问题,GitHub 上有一个问题

我确认如果我将 record 更改为 class,则不会出现此问题。

(我个人认为,当 Equals() 在一个 record 上,并且两个类型不同时,FluentAssertions 应该忽略它,因为这种行为可能不是人们所期望的。当前发布时的版本为 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; }
    }
}

你能否编辑问题,将AddressAddressDto的定义包含进去? - canton7
没问题,@canton7;稍等一下。 - Leaky
partnerDto变量的类型是什么?Address属性如何定义? - Pavel Anikhouski
1
@Leaky,为了能够重现您的问题,我们需要所有这些定义。 - Pavel Anikhouski
2
请注意,这个问题有一个开放的讨论 https://github.com/fluentassertions/fluentassertions/issues/1451 - Jonas Nyrup
显示剩余3条评论
2个回答

11

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

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


1
实际上这解决了问题。@canton7发表了一个很好的解释(可惜后来被删除了),建议使用 o.ComparingByMembers<AddressDto>(),但是并没有起作用。但是使用 Address 作为参数确实可以起作用。我真的不明白为什么;我还以为我应该用dto名称来给这个方法加参数。 - Leaky

9
我认为这篇文章中的重点是:

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

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

它使用两个记录的 ToString()版本报告失败,其中一个记录没有覆盖ToString返回{ Country = Canada,ZipCode = 12345,City = Toronto,Street = 4 Some street },另一个记录使用自定义的ToString方法返回 4 Some street,12345 Toronto,Canada,

正如文档所说,您可以通过使用 ComparingByMembers 来覆盖此行为:

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

或者全局:

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

这对我来说听起来非常合理,但是使用 options => options.ComparingByMembers<AddressDto>() 并没有改变结果。 :/ - Leaky
1
根据Matt Hope的回答,已编辑使用“Address”。请将他们的回答视为第一个、正确的、被接受的答案,并将我的回答视为添加额外背景信息。 - canton7
1
谢谢。我也认为在这里有这个解释很好,我确实接受了Matt Hope的答案,因为他是第一个回答的。不过,我还需要再研究一下,因为我也认为我应该使用另一种类型(DTO)来参数化这个方法。 - Leaky

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接