As .NET developers, we all know that the notion of object equality is not as simple as it looks first. Object equality is in fact probably as fundamental and difficult to understand as the famous concept of pointers in the good old C language. The fact is that writing unit tests, with Gallio/MbUnit or with any other existing framework is mostly about making equality assertions on the output of various code components. Thus chances are high you be stuck soon or later by an equality assertion which should obviously pass, but unexpectedly fails because your type does not implement any reliable equality mechanism. That's why I would like to review in that article, some features of MbUnit v3.1 which may help you using properly the powerful equality assertions.Let's start with a simple example.
[TestFixture]
public class SolarSystemTest
{
[Test]
public void Number_of_planets_in_the_solar_system()
{
var repository = new StarSystemRepository();
var solarSystem = repository.GetLocalSystem();
int count = solarSystem.CountPlanets();
Assert.AreEqual(8, count);
}
}
We use here the well-known
Assert.AreEqual
to verify that the actual number of planets found in our solar system is 8, as expected. Of course, the equality assertion knows how to compare System.Int32
values. Basically, it knows how to compare any primitive like System.String
or System.Double
. But what happens while asserting on non-primitive types?By default, MbUnit relies on the result returned by the overridable Object.Equals
method. And by default, that method simply consists of a referential equality. Thus it returns true only if the 2 objects compared represent the same instance (EDIT: OK, let's ignore the case where objects are null for the moment.). That's why, given our implementation of the class Planet
, the following test miserably fails.public class Planet
{
public string Name
{
get;
private set;
}
public Planet(string name)
{
Name = name;
}
}
[TestFixture]
public class SolarSystemTest
{
[Test]
public void Mercury_is_the_closest_planet_to_the_sun()
{
var repository = new StarSystemRepository();
var solarSystem = repository.GetLocalSystem();
IPlanet actual = solarSystem.GetClosestPlanetToTheStar();
Assert.AreEqual(new Planet("Mercury"), actual); // Fail!?
}
}
So how should we write the assertion to get the expected result? There are several solutions.
Asserting the inner object properties.
The most obvious solution is indeed to verify the inner properties of the actual object. We could just replace the failing assertion by:Assert.AreEqual("Mercury", actual.Name);
For a simple scenario such as in our example, this is surely enough. But what ifPlanet
is in fact a complex entity, with a dozen of properties, each hiding a complex tree of entities and value objects? At best, you would end up with a very large number of unmaintainable assertions. That's why you should avoid that solution if you get more than 3 or 4 assertions.Supporting equality.
The second possibility consists in implementing a dedicated equality mechanism for the evaluated type. The standard way of doing it is to implement theIEquatable<T>
interface.public class Planet : IEquatable<Planet>
This may sound like a perfectly reasonable solution. In fact, if you are a lucky developer, chances are good that your class already implements such an equality mechanism. It is perhaps a needed feature of your code base. If yes, then look no further, and just use it. The initial assertion will pass. Congratulations! You made it.But if no, then
{
public string Name
{
get;
private set;
}
public Planet(string name)
{
Name = name;
}
public bool Equals(Planet other)
{
return (other != null) && (Name == other.Name);
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
public override bool Equals(object obj)
{
return Equals(obj as Planet);
}
}Planet
has no equality mechanism probably because it does not need of any. It means that you are about to add an unnecessary feature to your code base, just to make your tests easier to write. Remember YAGNI? You ain't gonna need it! IfPlanet
does not need to be equatable, then why making it equatable? Your answer should never be: "To make it more testable". This is a short way to the dark side of the force, I assure you. Adding unnecessary code that will only eventually be used by your unit tests is certainly a bad practice.Using the comparison delegate.
Most of the MbUnit equality assertions take a third optional parameter of the typeEqualityComparison<T>
. The equality comparison delegate is a function which takes two instances of the same type, and returnstrue
if they are equal. If your scenario is simple enough, you can provide such a function to specify to the assertion how to compare the objects. With the lambda syntax, it looks very elegant.Assert.AreEqual(new Planet("Mercury"), actual, (x, y) => x.Name == y.Name);
And if the object has a reasonable number of properties, that's still a good solution.Assert.AreEqual(new Planet("Mercury"), actual, (x, y) =>
But again, if the type has too many properties, or if these properties are not more equatable than their parent, you will get a complete mess.
x.Name == y.Name &&
x.Weight = y.Weight &&
x.DistanceToStar == y.DistanceToStar);Using the structural equality comparer.
MbUnit v3.1 comes with a fantastic built-in feature which deserves to be better known (but that's the point of this post anyway). Basically, the structural equality comparer (StructuralEqualityComparer<T>
) provides to the assertions a convenient way to determine whether two instances of a type are equal or not; while the type itself does not implement any relevant equality mechanism.Assert.AreEqual(new Planet("Mercury"), actual,
Well, it does not look so impressive, does it? The comparer instance is populated with one comparison criterion that says to the assertion engine to use the property Name to compare two
new StructuralEqualityComparer<Planet>
{
{ x => x.Name }
});Planet
instances. The syntax is not much more complicated when you have several properties.Assert.AreEqualnew Planet("Mercury"), actual,
The true power appears when you know that each equality criterion is easily customizable, either with a comparison delegate, or with a new inner comparer.
new StructuralEqualityComparer<Planet>
{
{ x => x.Name },
{ x => x.Weight },
{ x => x.DistanceToSun }
});Assert.AreEqual(new Planet("Mercury"), actual,
As you see, each criterion is able to define its own comparison rules. You can also nest structural equality comparers and define them as a comparison rule for an inner criterion.
new StructuralEqualityComparer<Planet>
{
{ x => x.Name, (a, b) => a.Equals(b, StringComparison.OrdinalIgnoreCase) },
{ x => x.Weight },
{ x => x.DistanceToSun },
{ x => x.Revolution, (a, b) => a.Period == b.Period }
});Assert.AreEqual(new Planet("Mercury"), actual,
The comparer works very well with enumerations too. It provides a similar result to what do
new StructuralEqualityComparer<Planet>
{
{ x => x.Name, (a, b) => a.Equals(b, StringComparison.OrdinalIgnoreCase) },
{ x => x.Weight },
{ x => x.DistanceToSun },
{ x => x.Revolution, new StructuralEqualityComparer<Revolution> { { x => x.Period } } }
});Assert.AreElementsEqual
andAssert.AreElementsEqualIgnoringOrder
. Suppose thatPlanet
has now a property named Satellites which returns an instance of the typeIEnumerable<Satellite>
. Adding them into the overall comparison structure is easy. The comparer also supports some options to ignore the order of the child elements.Assert.AreEqual(new Planet("Mercury"), actual,
As already explained, the structural equality comparer can be used with most of the equality assertions. It is particularly useful with the equality assertions for collections.
new StructuralEqualityComparer<Planet>
{
{ x => x.Name, (a, b) => a.Equals(b, StringComparison.OrdinalIgnoreCase) },
{ x => x.Weight },
{ x => x.DistanceToSun },
{ x => x.Revolution, (a, b) => a.Period == b.Period }
{ x => x.Satellites, new StructuralEqualityComparer<Satellite>
{
{ x.Name },
{ x.DistanceToPlanet }
}, StructuralEqualityComparerOptions.IgnoreEnumerableOrder
}
});Assert.AreElementsEqualIgnoringOrder(
new[] { new Satellite("Deimos"), new Satellite("Phobos") },
mars.Satellites,
new StructuralEqualityComparer<Satellite> { { x => x.Name } });
2 comments:
Hi Yann, these posts on MbUnit features are very good. I'm fairly new to MbUnit and think it's a great framework.
Thank you Kevin, for your blog article.
Post a Comment