使用 FluentAssertions 测试可选等价性

Testing Optional equivalence with FluentAssertions

我正在使用一个名为 Optional (https://github.com/nlkl/Optional) 的库,它允许函数式语言常见的“可能”抽象。

库很棒,但我遇到了一个测试问题:我无法正确测试 2 个可选实例是否等价。

为了测试等效性,我使用了 Fluent Assertions。但是,我没有得到想要的结果。

我用代码说明问题:

#load "xunit"

[Fact]
void TestOptional()
{
    var a = new[] { 1, 2, 3 }.Some();
    var b = new[] { 1, 2, 3 }.Some();
    
    a.Should().BeEquivalentTo(b);
}

此测试失败,如我在屏幕截图中所示(为方便起见,我使用的是 LINQPad)

如您所见,这不是人们所期望的。

如何告诉 Fluent Assertions 使用 Option 类型正确检查等价性?

更新

I opened an issue on Github regarding your problem and yesterday a pull request was merged,所以下一个(预)版本应该能让你优雅地解决你的问题:

The new overloads allows you to use an open generic type. If both an open and closed type are specified, the closed type takes precedence.

SelfReferenceEquivalencyAssertionOptions 添加以下方法:

  • public TSelf ComparingByMembers(System.Type type) { }
  • public TSelf ComparingByValue(System.Type type) { }

Here's the unit test 添加到 Fluent Assertions 中,展示其工作原理:

[Fact]
public void When_comparing_an_open_type_by_members_it_should_succeed()
{
    // Arrange
    var subject = new Option<int[]>(new[] { 1, 3, 2 });
    var expected = new Option<int[]>(new[] { 1, 2, 3 });

    // Act
    Action act = () => subject.Should().BeEquivalentTo(expected, opt => opt
        .ComparingByMembers(typeof(Option<>)));

    // Assert
    act.Should().NotThrow();
}

Fluent Assertions - Object Graph Comparison 说:

Value Types

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. Unfortunately, anonymous types and tuples also override this method, but because we tend to use them quite often in equivalency comparison, we always compare them by their properties.

You can easily override this by using the ComparingByValue<T> or ComparingByMembers<T> options for individual assertions

Option<T>struct 并覆盖 Equals,因此 Fluent Assertions 将 ab 与值语义进行比较。

Option<T> 像这样实现 Equals

public bool Equals(Option<T> other)
{
  if (!this.hasValue && !other.hasValue)
    return true;
  return this.hasValue
    && other.hasValue 
    && EqualityComparer<T>.Default.Equals(this.value, other.value);
}

因此 int[] 通过引用进行比较,您的测试失败。

您可以为每个测试单独覆盖此行为,如 Guro Stron 所说:

a.Should().BeEquivalentTo(b, opt => opt.ComparingByMembers<Option<int[]>>());

或全局通过静态 AssertionOptions class:

AssertionOptions.AssertEquivalencyUsing(options => 
    options.ComparingByMembers<Option<int[]>>());

编辑:

对于您的情况,Fluent Assertions 需要一个 AssertEquivalencyUsing 支持未绑定泛型类型的覆盖:

AssertionOptions.AssertEquivalencyUsing(options => 
    options.ComparingByMembers(typeof(Option<>)));

不幸的是,不存在这样的覆盖。

a 提出的另一种解决方案是扩展方法。这里有一个非常简单的实现:

public static class FluentAssertionsExtensions
{
    public static void BeEquivalentByMembers<TExpectation>(
        this ComparableTypeAssertions<TExpectation> actual,
        TExpectation expectation)
    {
        actual.BeEquivalentTo(
            expectation,
            options => options.ComparingByMembers<TExpectation>());
    }
}