Search code examples
c#unit-testingfluent-assertionsoption-type

Testing Optional equivalence with FluentAssertions


I'm using a library called Optional (https://github.com/nlkl/Optional) that allows the "maybe" abstraction that is common to functional languages.

The library is awesome, but I'm facing a problem regarding testing: I can't test correctly whether 2 optional instances are equivalent or not.

To test for equivalence I'm using Fluent Assertions. However, I'm not getting the desired results.

I will illustrate the problem with code:

#load "xunit"

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

This test fails, as I show in the screenshot (I'm using LINQPad, for convenience)

enter image description here

As you see, this isn't what one expects.

How do I tell Fluent Assertions to check the equivalence correctly using the Option type?


Solution

  • UPDATE

    I opened an issue on Github regarding your problem and yesterday a pull request was merged, so the next (pre-)release should enable you to solve your problem elegantly:

    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 adds the following methods:

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

    Here's the unit test that was added to Fluent Assertions showing how it works:

    [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 says:

    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> is a struct and overrides Equals, so Fluent Assertions compares a and b with value semantics.

    Option<T> implements Equals like this:

    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);
    }
    

    Thus int[] is compared by reference and your test fails.

    You can override this behavior for each test individually, like Guro Stron said:

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

    Or globally via static AssertionOptions class:

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

    edit:

    For your case Fluent Assertions would need an AssertEquivalencyUsing override that supports unbound generic types:

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

    No such override exists, unfortunately.

    Another solution a came up with would be an extension method. Here a very simplistic implementation:

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