11

I'm writing some unit tests and the following assertion fails:

Assert.AreEqual(expected.Episode, actual.Episode);

If I call this instead, it succeeds:

Assert.IsTrue(expected.Episode.Equals(actual.Episode));

I had assumed that Assert.AreEqual() ultimately calls the Equals() method for the type it is given, in this case Episode.Equals().

However, under the covers in Microsoft.VisualStudio.TestTools.UnitTesting.Assert I found the following code (decompiled by ReSharper):

public static void AreEqual<T>(T expected, T actual, string message, params object[] parameters)
{
    if (object.Equals((object)expected, (object)actual))
        return;
    Assert.HandleFail...
}

This implies to me that the AreEqual() method is casting both expected and actual to object to force the use of the base Equals() method rather than the overload I have written in the Episode class. The base method will simply check to see if the references are the same, which they are not.

I have two questions:

  1. Is my explanation actually correct, or have I missed something?
  2. Why would the framework want to force use of object.Equals() rather than an overload of that method?

If it's relevant, here is my method:

public bool Equals(Episode other)
{
    return Number == other.Number &&
           CaseNote.Equals(other.CaseNote) &&
           Patient.Equals(other.Patient);
}
Sir Crispalot
  • 4,792
  • 1
  • 39
  • 64
  • Offtopic: which version of ReSharper can decompile? – sll Nov 14 '12 at 14:28
  • I'm not sure how long it's been there but at least v5 in my experience ... the first time you F12 (go to declaration) on an internal object it will ask what you want to do; one of the options is to decompile. There is also a free standalone decompiler from Jetbrains. – Sir Crispalot Nov 14 '12 at 14:30
  • 1
    btw, the method shown is an *overload*, not an `override`. – Marc Gravell Nov 14 '12 at 14:33

2 Answers2

6

It is using object.Equals(object,object), which deals with things like:

  • are they the same reference?
  • is either or both a null reference?

and then goes on to use x.Equals(y) after it has handled those things. It has to cast them to object because that is what object.Equals(object,object) takes. Casting to object also avoids some complications with Nullable<T> (because a T? boxes either to null or to a regular boxed T).

However, it could also have been implemented as:

 if (EqualityComparer<T>.Default.Equals(expected,actual))
    return;

which handles Nullable<T>, IEquatable<T>, struct vs class, and a few other scenarios without any boxing.

But: the current implementation does the job, and the occasional box isn't the end of the world (and: boxing isn't even an issue if your type is a class).

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Note that [checking for null by casting to object on `Nullable` has a performance impact that can be avoided](http://stackoverflow.com/q/12396457/50776) (it's not your fault, mind you). – casperOne Nov 14 '12 at 14:36
  • @casperOne indeed, which is why *if I was writing it* I would have used `EqualityComparer`, which **does not** suffer this. – Marc Gravell Nov 14 '12 at 14:38
  • While this answer is correct, it fails to explain why the OP is seeing false when he expects to see true. The main issue here is that he only implemented `Equals(Episode other)` and not `Equals(object other)` as well. – Servy Nov 14 '12 at 15:02
4

In your code you need to override Equals(object other) as well (and need to override GetHashCode too).

Just add this to your code

public bool Equals(Episode other)
{
    return Number == other.Number &&
           CaseNote.Equals(other.CaseNote) &&
           Patient.Equals(other.Patient);
}

public override bool Equals(object other)
{
    Episode castOther = other as Episode;
    if(castOther == null)
        return false;
    return this.Equals(castOther);
}

public override int GetHashCode()
{
    //TODO: Implement using the members you used in "Equals(Episode other)"
    throw new NotImplmentedExecption();
}

Remember for GetHashCode if two objects are equal they must also return equal hash codes. Here is a quick diagram to help visualize.

enter image description here

You may want to check CaseNote and Patient for similar issues.

Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • Thanks for your helpful answer. I realise that this is *actually* the reason that my code returns different results for the two assertions. – Sir Crispalot Nov 15 '12 at 09:03