2008/09/03

Contract Verifiers - Part III. - The Equality Contract Verifier

The path that leads to a proper implementation of the equality contract has many pitfall traps. Fortunately, the equality contract verifier will help you in avoiding them. For convenience, we will consider as an example, a hypothetical class representing a simple mathematical fraction which implements IEquality<T>.
public class Fraction : IEquality<Fraction>
{
public Fraction(int numerator, int denominator)
{
// ...
}

// ...
}
In order to test the type implementing the generic equality interface, the contract verifier needs some valid type instances. That's why the test fixture must fulfill a two side parts contract.
  • It must have the attribute VerifyEqualityContract.
  • It must implement the interface IEquivalenceClassProvider<T>, which will provide the test runner with various object instances to play with.
using Gallio.Framework;
using MbUnit.Framework;
using MbUnit.Framework.ContractVerifiers;

[TestFixture]
[VerifyEqualityContract(typeof(Fraction))]
public class FractionTest : IEquivalenceClassProvider<Fraction>
{
public EquivalenceClassCollection<Fraction> GetEquivalenceClasses()
{
return EquivalenceClassCollection<Fraction>.FromDistinctInstances(
new Fraction(1, 2),
new Fraction(2, 3),
new Fraction(4, 5));
}
}
The IEquivalenceClassProvider<T> has a single method that you must use to return a collection of equivalence classes for the evaluated type. An equivalence class is a collection of object instances that are expected to be equal (The notion of equality is of course to be understood in the sense of your own equality contract). In the simplest scenarios, it is generally sufficient to provide several distinct object instances to be compared together. Use the factory method FromDistinctInstances to construct such a collection of equivalence classes. In the previous example, we provide 3 equivalence classes, each containing 1 single object.

However, in more complex scenarios, we want to verify that different object instances "representing" the same value are actually equal. For instance, the fraction 1/2 should be equal to the fraction 2/4 or 3/6. Instead of using the factory method, use the constructor to provide some equivalence classes containing a variable number of objects.
[TestFixture]
[VerifyEqualityContract(typeof(Fraction))]
public class FractionTest : IEquivalenceClassProvider<Fraction>
{
public EquivalenceClassCollection<Fraction> GetEquivalenceClasses()
{
return new EquivalenceClassCollection<Fraction>(
new EquivalenceClass<Fraction>(
new Fraction(1, 2),
new Fraction(2, 4),
new Fraction(3, 6)),
new EquivalenceClass<Fraction>(
new Fraction(2, 3),
new Fraction(4, 6)),
new EquivalenceClass<Fraction>(
new Fraction(4, 5)))
}
}
Now that we have defined our contract verifier, let's see what it will check for.

1. It verifies that the method GetHashCode returns the same value for equal objects. You should override the method and return an identical value for all the objects located in the same equivalence class. However, the contrary is not true: it is not required that the resulting hash value for unequal objects be different (although it would be a good idea, for efficiency reasons)
public class Fraction : IEquality<Fraction>
{
public override int GetHashCode()
{
// ...
}
}
2. It verifies that the method Object.Equals(object) behaves as expected. The method should return true when two instances of the same equivalence class are compared together, or when an instance is compared to itself. For nullable types (reference types, or value types nested in a Nullable class), it verifies you dealt correctly with null references. The method should return False when called against a null reference.
public class Fraction : IEquality<Fraction>
{
public override bool Equals(object obj)
{
// ...
}
}
3. Same verifications as point 2; but for the strongly typed version of the Equals method (which is actually the only method that the IEquatable<T> interface requires you to implement).
public class Fraction : IEquality<Fraction>
{
public bool Equals(Fraction other)
{
// ...
}
}
Whenever implementing IEquatable<T>, it is usually recommended to overload the two equality operators as well. By default, the equality contract verifier will evaluate them too. If your class does not override those operators, consider disabling that feature by setting to false the named parameter ImplementsOperatorOverload.
[TestFixture]
[VerifyEqualityContract(typeof(Fraction), ImplementsOperatorOverload = false)]
public class FractionTest : IEquivalenceClassProvider<Fraction>
{
// ...
}
4. It verifies that the equality operator (==) behaves as expected. The verifications are the same as for points 2 and 3. For nullable types, it is also verified that two null references are considered to be equal.
public class Fraction : IEquality<Fraction>
{
public static bool operator ==(Fraction left, Fraction right)
{
// ...
}
}
5. It verifies that the inequality operator (!=) behaves as expected.
public class Fraction : IEquality<Fraction>
{
public static bool operator !=(Fraction left, Fraction right)
{
// ...
}
}

No comments: