question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Should().Be() / NotBe() prefers IComparable<T>.Compare over object.Equals

See original GitHub issue

Given the following class with value semantics.

public class Ranked<T> : IComparable<Ranked<T>>
{
    public Ranked (int ranking, T name) { Ranking = ranking; Item = name; }

    public int Ranking { get; private set; }
    public T Item { get; private set; }

    public override bool Equals (object obj)
    {
        if (ReferenceEquals (null, obj))
            return false;
        if (obj.GetType() != this.GetType())
            return false;
        var other = (Ranked<T>) obj;
        return Ranking == other.Ranking && Equals (Item, other.Item);
    }

    public override int GetHashCode () { /* ... */ }

    public int CompareTo (Ranked<T> other)
    {
        if (other == null) return 1;

        return this.Ranking.CompareTo (other.Ranking);
    }
}

At the moment (v3.2.1) the assertion new Ranked<string>(1, "Some item").Should().Be (new Ranked<string>(1, "Other item")) passes whereas casting the first part to object, ((object)new Ranked<string>(1, "Some item")).Should().Be (new Ranked<string>(1, "Other item")), fails. The first assertion calls CompareTo, which just compares the Ranking value and the second one calls Equals.

The reason for the different behavior of the two assertions is the IComparable<> overload of Should(). The idea of this overload is to be able to offer fluent methods for IComparable<> values, like BeInRange(). Unfortunately also Be() and NotBe() are “overridden” to check for CompareTo() == 0 resp. CompareTo() != 0.

In my opinion this is not a good idea, because Be() / NotBe() are the standard methods in FluentAssertions to check for equality / inequality. And sort ordering is not the same as equality. The example above shows a legitimate case, where just a subset of the properties is part of the ordering.

Thinking of another example of a Person-entity where equality is defined by ID-equality (e.g. a SocialSecurityNumber property) and the ordering should be applied for LastName / GivenName shows that the set of properties used for equality / ordering don’t even have to intersect.

We should think about a (breaking) change of the behavior of ComparableTypeAssertions<T>.Be(). So that Should().Be() always checks for equality and introduce a new Should().HaveSameOrderingPositionAs() (probably there’s better name), which checks for CompareTo() == 0.

The reason why I would even prefer the breaking change is that a) it’s unintuitive that Should.Be() delegates to CompareTo depending on the compile time type of the left operand and b) the duality of Should().Be() is unsafe for example when adding IComparable<T> to the Ranked<T> example above after there are already a lot of unit tests, which use Should.Be() and rely on its behavior.

Further notes:

  • The MSDN documentation team strengthened the definition of the zero result of IComparable.CompareTo from to “This instance is equal to obj” to “This current instance occurs in the same position in the sort order as the object specified by the CompareTo method” in .NET 3.5 (obviously to distinguish between equality and sort ordering). Interestingly they didn’t strengthen the definition of the generic version, but the semantics are the same for both interfaces. (This happened in the meantime.)
  • NUnit’s Assert.AreEqual and Assert.That only use IEquatable<>.Equals and object.Equals (depending on the runtime type), which both have the same semantics.
  • MSTest’s Assert.AreEqual only uses object.Equals.

Issue Analytics

  • State:closed
  • Created 9 years ago
  • Comments:11 (10 by maintainers)

github_iconTop GitHub Comments

4reactions
jnyrupcommented, Jan 11, 2020

5 years later, this was fixed in #1177.

2reactions
rubenrorijecommented, Sep 24, 2019

Since this issue is open for quite a while now, would it be a solution for the time being to change the implementation of Be in ComparableTypeAssertions to also check equality? It would be a breaking change, but would only result in more assertions and would catch more errors that previously would be unnoticed. Note that it assumes that equality is implemented when implementing IComparable, but this seems like a safe assumption to me.

It would not solve my problem above, but I am more concerned about my and other developer’s incorrect expactations than my issue above, that is easily circumvented.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Should be IEquatable<T>'s Equals() be implemented via ...
According to Eric Lippert, a former developer on the C# compiler team at Microsoft: There are nine ways to do a comparison in...
Read more >
Object graph comparison
You may assert the structural equality of two object graphs with Should(). ... sections are available for both BeEquivalentTo and NotBeEquivalentTo .
Read more >
"Equals" and the comparison operators should be ...
This rule raises an issue when a class implements IComparable without also overriding Equals(object) and the comparison operators. Noncompliant code example.
Read more >
Implementing class equality/comparison in F# - ...
The code works the same if I don't implement IEquatable, as it uses the Equals() override. Is it considered desirable to implement IEquatable ......
Read more >
Story of Equality in .NET
In this post we will go through, what the equality operator does and how it works under the hood in detail, so after...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found