Testing for object equality in C# (common in TDD)
Posted: (EET/GMT+2)
Assume you are writing your application and want to focus on unit testing. Perhaps you are working with the TDD methodology, writing your tests before your code. For example, you could be writing your ASP.NET MVC application, and want to write a unit test to compare two model objects. Say, you have the two following classes:
public class Cat
{
public string Name { get; set; }
}
public class Dog
{
public string Name { get; set; }
}
In .NET and C#, the base class for all objects, System.Objects implements the Equals method that by default compares objects by reference (for reference types). Thus, when you say something like "if (a == b)" and both a and b are class instances, then the default implementation compares whether the object references to the same memory location. This is called reference equality.
However, you cannot compare two different classes by default. For example, the following code:
Cat c = new Cat();
Dog d = new Dog();
if (c == d)
{
MessageBox.Show("Cat IS equal to dog.");
}
else
{
MessageBox.Show("Cat is NOT equal to dog.");
}
...would cause the following compiler error CS0019 on the third line:
Error: Operator '==' cannot be applied to operands of type 'EqualityTest.Cat' and 'EqualityTest.Dog'
If you wanted to compare a cat to a dog (as would be common when working with TDD), the solution would be to override the Equals method in both the Cat and Dog classes. Also, if you choose to implement your own Equals method, you might actually wish to compare the values of the classes (for example, some property value(s) like the Name above) instead of just memory references. This is called testing for value equality.
But, although implementing the Equals method sounds easy, you actually should do things properly. The MSDN documentation for Equals lists several rules that all implementations should follow. The following is a quote from the documentation:
The following statements must be true for all implementations of the Equals method. In the list, x, y, and z represent object references that are not null.
- - x.Equals(x) returns true, except in cases that involve floating-point types. See IEC 60559:1989, Binary Floating-point Arithmetic for Microprocessor Systems.
- - x.Equals(y) returns the same value as y.Equals(x).
- - x.Equals(y) returns true if both x and y are NaN.
- - If (x.Equals(y) && y.Equals(z)) returns true, then x.Equals(z) returns true.
- - Successive calls to x.Equals(y) return the same value as long as the objects referenced by x and y are not modified.
- - x.Equals(null) returns false.
In practice, I've myself followed a Rule of Four. This means that when implementing the Equals method for classes and I want to test for value equality, I create four different tests inside the methods. These are:
- Test for null values. The object that we compare to cannot be null.
- Compare types with GetType(). Usually, you don’t want to compare apples to oranges, or cats or dogs in this case.
- Check for reference equality. If by chance the two objects point to the same memory location, then there’s no need to check for field-by-field value equality. This is a speed optimization.
- Check for value equality, field-by-field (or property-by-property).
These four tests should correctly implement the rules set forth in MSDN.
Note that when you override the Equals method in your own class, you should also override the GetHashCode method. This is not strictly necessary if you do not plan to use for example the Hashtable or derived collection classes, but nonetheless, it is a good practice to do so. If you don't, the C# compiler will issue a warning to the following effect:
Warning: 'EqualityTest.Cat' overrides Object.Equals(object o) but does not override Object.GetHashCode()
Here's the final code for the Cat and Dog classes.
public class Cat
{
public string Name { get; set; }
public override bool Equals(object obj)
{
// test 1: null value
if (obj == null) return false;
// test 2: different types
if (this.GetType() != obj.GetType()) return false;
// test 3: reference equality (for speed)
if (object.ReferenceEquals(this, obj)) return true;
// test 4: value equality
return this.Name == ((Cat)obj).Name;
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
}
public class Dog
{
public string Name { get; set; }
public override bool Equals(object obj)
{
// test 1: null value
if (obj == null) return false;
// test 2: different types
if (this.GetType() != obj.GetType()) return false;
// test 3: reference equality (for speed)
if (object.ReferenceEquals(this, obj)) return true;
// test 4: value equality
return this.Name == ((Dog)obj).Name;
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
}
To test these, call the Equals method directly like this:
if (c.Equals(d)) ...
To use the == operator, you would still need to write additional code to overload the equality operator.