8
\$\begingroup\$

I've created this abstract base class for testing the Equals method.

The basic idea is that derived class implements methods to get:

  • the primary test instance, and
  • a list of unequal instances (it's up to the derived class to ensure that this list is sufficient instances for testing all possible inequalities)

The base class then is responsible for using those instances to test.

Am I missing any unit tests that would be useful for ensuring that the class is correctly using Equals and GetHashCode?

using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// A base class for testing the Equals method of an object.
/// </summary>
[TestClass]
public abstract class EqualityTestBase
{
    [TestMethod]
    public void Equals_PassNull_ReturnFalse()
    {
        // -------- Assemble --------
        object target = GetPrimary();
        bool expected = false;

        // -------- Act --------
        bool actual = target.Equals(null);

        // -------- Assert -------- 
        Assert.AreEqual(expected, actual);
    }

    [TestMethod]
    public void Equals_PassGenericObject_ReturnFalse()
    {
        // -------- Assemble --------
        object target = GetPrimary();
        object genericObject = new object();
        bool expected = false;

        // -------- Act --------
        bool actual = target.Equals(genericObject);

        // -------- Assert -------- 
        Assert.AreEqual(expected, actual);
    }

    [TestMethod]
    public void Equals_EquivalentObject_ReturnTrue()
    {
        // -------- Assemble --------
        object target = GetPrimary();
        object equivalent = GetPrimary();
        bool expected = true;

        // -------- Act --------
        bool actual = target.Equals(equivalent);

        // -------- Assert -------- 
        Assert.AreEqual(expected, actual);
    }

    [TestMethod]
    public void Equals_SameObject_ReturnTrue()
    {
        // -------- Assemble --------
        object target = GetPrimary();
        bool expected = true;

        // -------- Act --------
        bool actual = target.Equals(target);

        // -------- Assert -------- 
        Assert.AreEqual(expected, actual);
    }

    [TestMethod]
    public void Equals_DifferentObjects_NoneMatch()
    {
        // -------- Assemble --------
        object target = GetPrimary();
        IEnumerable<object> nonEquivalents = GetNonEquivalentObjects();
        bool expected = false;

        // -------- Act --------
        bool anyEqual = nonEquivalents.Any(nonEquivalent => target.Equals(nonEquivalent));

        // -------- Assert -------- 
        Assert.AreEqual(expected, anyEqual);
    }

    [TestMethod]
    public void HashCode_EquivalentObject_BothSame()
    {
        // -------- Assemble --------
        object target = GetPrimary();
        object equivalent = GetPrimary();
        bool expected = true;

        // -------- Act --------
        bool actual = target.GetHashCode() == equivalent.GetHashCode();

        // -------- Assert -------- 
        Assert.AreEqual(expected, actual);
    }

    [TestMethod]
    public void HashCode_SameObject_BothSame()
    {
        // -------- Assemble --------
        object target = GetPrimary();
        bool expected = true;

        // -------- Act --------
        bool actual = target.GetHashCode() == target.GetHashCode();

        // -------- Assert -------- 
        Assert.AreEqual(expected, actual);
    }

    [TestMethod]
    public void HashCode_DifferentObjects_NoneMatch()
    {
        // -------- Assemble --------
        object target = GetPrimary();
        IEnumerable<object> nonEquivalents = GetNonEquivalentObjects();
        bool expected = false;

        // -------- Act --------
        bool anyEqual = nonEquivalents.Any(nonEquivalent => target.GetHashCode() == nonEquivalent.GetHashCode());

        // -------- Assert -------- 
        Assert.AreEqual(expected, anyEqual);
    }

    /// <summary>
    /// Get the primary object for testing equality.
    /// </summary>
    /// <returns>The primary object</returns>
    protected abstract object GetPrimary();

    /// <summary>
    /// Get the list of objects which should not equal the primary object.
    /// </summary>
    /// <returns>List of non equivalent objects</returns>
    protected abstract IEnumerable<object> GetNonEquivalentObjects();
}
\$\endgroup\$
  • \$\begingroup\$ HashCode_DifferentObjects_NoneMatch is wrong. From GetHashCode documentation "Do not test for equality of hash codes to determine whether two objects are equal. (Unequal objects can have identical hash codes.)..." [emphasis added] \$\endgroup\$ – abuzittin gillifirca Jan 21 '14 at 9:42
5
\$\begingroup\$

It seems to be complete but keep in mind that equality is reflexive, symmetric, and transitive so, as a formality, I would add tests:

  • a.Equals(b) has the same value as b.Equals(a)
  • if a.Equals(b) and b.Equals(c) are both true, then a.Equals(c) is also true
\$\endgroup\$
3
\$\begingroup\$

In addition to Konrad's suggestions a few remarks to some of the test themselves:

  1. If you make the base class generic then you can actually have a type for your primary instead of passing it through a plain object.

  2. I'm not that fond of the naming of the test cases, they don't rally read that fluently. Some suggestions:

    • PrimaryIsNotEqualToNull
    • PrimaryIsNotEqualToGenericObject
    • PrimaryIsEqualToEquivalentPrimary
    • PrimaryIsEqualToItself
    • ...
  3. I would change the abstract interface to two methods:

    • GetSimilarPrimary - should return primary objects which are all similar to each other and should compare equal (but they are different objects)
    • GetDifferentPrimary - should return primaries which are all different from each other and never compare equal to each other.

    This make their usage clearer and you can implement the IEnumerable of different objects in the base class yourself and the derived class doesn't need to care about it.

  4. If you do the above then I'd add two test cases to make sure the implementation conforms to the requirement and make sure that objects returned from GetSimilarPrimary always compare equal and objects from GetDifferentPrimary always compare false. This catches programming errors in the unit test implementation for the derived classes.

  5. In this test:

    [TestMethod]
    public void HashCode_EquivalentObject_BothSame()
    {
        // -------- Assemble --------
        object target = GetPrimary();
        object equivalent = GetPrimary();
        bool expected = true;
    
        // -------- Act --------
        bool actual = target.GetHashCode() == equivalent.GetHashCode();
    
        // -------- Assert -------- 
        Assert.AreEqual(expected, actual);
    }
    

    You compare the hashcodes and then compare the result of the comparison which is a bit roundabout and will make the test result a little bit harder to read in case of failure (two bools are different rather than two ints). I'd change that to:

    [TestMethod]
    public void HashCode_EquivalentObject_BothSame()
    {
        // -------- Assemble --------
        object target = GetPrimary();
        object equivalent = GetPrimary();
    
        // -------- Act --------
        int targetHash = target.GetHashCode();
        int equivalentHash = equivalent.GetHashCode();
    
        // -------- Assert -------- 
        Assert.AreEqual(targetHash , equivalentHash);
    }
    

    If you want keep something for the Act part then introduce two local variables to store the hash codes.

  6. There is also the general requirement that if two objects compare as equal then their hash codes should be equal. While this is implicitly caught by the tests I'd actually consider adding another test which checks that explicitly.

\$\endgroup\$
  • \$\begingroup\$ Thanks. WRT #1, this would be ideal, but VS2010 won't pick up the unit tests if I make it generic. It's a known issue. WRT #2 & 5, that's policy where I work. WRT #3 & 4, good call. WRT #6, I didn't know that. Thanks, I'll update my tests. \$\endgroup\$ – Robert Gowland Jan 22 '14 at 20:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.