Any class or structure in .NET can define the equality and inequality operators. Implementing such operators, however, can be error-prone. The operators therefore need to be validated by unit tests. We will first discuss the challenges of writing such unit tests. We will then review how just one new NUnit constraint can help writing thorough tests for the equality and inequality operators.
The implementations of the equality and inequality operators are presented below for a simple
Customer class: two customers are expected to be equal if they have the same name.
Refer to the Microsoft guidelines for further details.
Such implementations typically use
rhs as names for the parameters of the equality and inequality operators.
lhs is the abbreviation for left-hand side.
rhs stands for right-hand side.
For the remaining of this discussion, equality operators will refer to both the equality and inequality operators.
Equality operators allow to express equality relations between two objects using a more natural syntax than the overridden
Equals method inherited from
object, the base type of the .NET run-time.
Fully supporting equality and inequality relations requires to respect three semantics:
Testing the semantics of the operators can be done with NUnit constraints.
Given any references
c3 of type
Customer, the equality operators should be:
Equality operators in .NET should also behave in relation with the
GetHashCode methods. Like the equality operators,
Equals should also be reflective, symmetric, and transitive.
Note that in the following piece of code,
c2 are assumed to be non-null.
Tests of the reflexivity, symmetry, and transitivity of
Equals are also omitted for brevity here.
Furthermore, any non-null reference
c of type
Customer should be expected not to be equal to the null reference.
Similarly, the static equality operators should behave properly when all the input parameters are null. Testing such extreme cases is important as developers, including myself, sometimes forget to deal appropriately with null references.
Challenges of Testing
Using equality operators allows to create rich but syntaxically simple expressions which are easy to read and maintain.
Therefore, an application may define a lot of equality operators. As mentioned previously, testing the newly implemented operators consists in testing if the operators respect the inherent semantics and are consistent with
Testing properly the equality operators is finally more complex than expected as the number of potential cases to test is quite large.
Furthermore, tests need also to be run for two references of
Customer expected to be unequal.
Consequently, the NUnit assertions should to be factorized in order to be reused across multiple test fixtures.
Consider the following first draft which attempts to factorize the assertions into the function
This function is meant to test not only the
Customer class, but also other types such as the
Supplier class or the
ZipCode structure for example.
Since the only commonality between all the .NET types is the type
object, our first draft will have two input parameters
obj2 of type
The second assert will not work as indented because the decision of choosing the appropriate static operator is made at compile-time.
Hence, the equality operator defined for the type
object will always be called at run-time. In other words, the second assertion just checks if
obj2 are the same reference.
Generics will not work either for the same reason:
operator == is resolved at compilate-time, not at run-time.
Compiling the following piece of code will lead to the following error:
Operator '==' cannot be applied to operands of type 'T' and 'U'.
Comments have been made on this issue.
Value and Reference Types
As previously mentioned, it is important to test cases with null parameters. However, such tests on the equality operators is irrelevant for value types.
Abstract Data Type (ADT) like the
Money structures usually rely on equality operators.
But it is expected that testing the
Equals method with a null parameter on a value type will always return false:
The challenges of implementing a generic test for the equality operators have now been outlined. Several paths of action are possible to implement the solution with NUnit. One way is to inherit test fixtures from a base class which will have factorized the assertions for equality operators. Another way consists in extending NUnit by creating a new type of constraint. This constraint evaluates whether or not two instances expected to be equal follow the semantics of the equality operators.
Equality Operator Constraint
Let's review the details of the implementation of
We will first tackle on how equality operators can be resolved not at compile-time but at run-time.
IStaticEqualityOperatorProvider is used to resolve the static equality operators at run-time. This interface defines two methods. As their names suggest,
EvaluateStaticNotEqualOperator return the result for the expressions
lhs == rhs and
lhs != rhs, respectively.
Several implementations are provided:
ReflectiveStaticEqualityOperatorProvider<T>relies on reflection to call the correct
DelegatedStaticEqualityOperatorProvideraccepts delegates as parameters of the constructor. It allows to come up with an inline implementation of the provider using anonymous methods. The following piece of code create a provider for
Aggregate of Atomic Constraints
Creating a new constraint in NUnit consists in implementing two methods of the abstract class
bool Matches(object actual) and
void WriteDescriptionTo(MessageWriter writer).
In the current implementation,
EqualityOperatorConstraint constructs a compound constraint using
OrConstraint) operators to add all the atomic constraints testing the semantic of the equality operators and the behavior of the
Using the Equality Constraints
Let's go back to our first example with the
Testing the equality operators as well as testing
GetHashCode is just a matter of using the new
EqualityOperatorConstraint combined with
The constraints comes with two flavors:
EqualityOperatorConstraintevaluates if two references are expected to be equal.
InequalityOperatorConstraintevaluates if two reference are expected to be unequal.
The NUnit model provides a description when a constraint fails.
As mentioned previously, the
EqualityOperatorConstraint is an aggregate of simple constraints.
To avoid any verbose message, the description of the first constraint which fails is provided instead.
Consider the following case where the equality operator of the
Customer class has been improperly implemented.
Instead of comparing the names, the operator compares the length of the names.
The case testing the inequality between a customer called James and Williams will pass since the names are of different lengths. However, testing the inequality between James and Henry will fail with this error message:
A related test with the equality constraint this time will also fail because two customers with names of same length return a different hash code:
In order to fully cover the tested code, the
EqualityOperatorConstraint and the
InequalityOperatorConstraint need to be used with different pairs of instances expected to be equal and unequal, respectivelly.
The equality constraints can only carry out tests with the provided instances. So, as usual, keep an eye on code coverage while writing unit tests.
The source code for this post is available from github.