I came across a question on stackoverflow concerning comparing object for equality in unit test. The poster basically wants to get rid of series of assertions like this:
1 2 3 4 5 |
Assert.AreEqual(LeftObject.Property1, RightObject.Property1); Assert.AreEqual(LeftObject.Property2, RightObject.Property2); Assert.AreEqual(LeftObject.Property3, RightObject.Property3); ... Assert.AreEqual(LeftObject.PropertyN, RightObject.PropertyN); |
I agree, this is ugly. The more so if you have multiple tests that assert on the equality of these properties. You can always factor it out into a helper function but you still end up actually writing the comparisons yourself.
The selected answer doesn’t feel right. The proposal is to implement Equals() in the class for your tested object. This is not always desirable or even possible. Consider the case where your use case actually makes use of Equals() in its logic. There may already exist an implementation of Equals() that satisfies different needs than those of your test. Moreover, when overriding Equals(), there is more to it than just this single function. GetHashCode() must be implemented too … and correctly ! If you don’t implement GetHashCode(), you may end up with subtle or not-so-subtle bugs if your object gets stored as a dictionary key. In most cases, it will not be an issue because only a very few classes are actually used a dictionary keys. However, if you get into the habit of overriding Equals() without GetHashCode(), you can be bitten hard !!
One of the most favored answer is to use reflection to discover the distinct properties. This is the way to go. Code that solely exists for testing purposes should be kept away from the classes you test. However, I find the proposed solution sub-optimal. For one thing, the method is solely dedicated to testing and directly calls Assert.AreEqual(). For another, I don’t like that it automatically recurse into IList properties, but this is a question of style.
I would propose a general purpose utility method like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/// <summary> /// Returns the names of the properties that are not equal on a and b. /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <returns>An array of names of properties with distinct values or null if a and b are null or not of the same type</returns> public static string[] GetDistinctProperties(object a, object b) { if (object.ReferenceEquals(a, b)) return null; if (a == null) return null; if (b == null) return null; var aType = a.GetType(); var bType = b.GetType(); if (aType != bType) return null; var props = aType.GetProperties(); if (props.Any(prop => prop.GetIndexParameters().Length != 0)) throw new ArgumentException("Types with index properties not supported"); return props .Where(prop => !Equals(prop.GetValue(a, null), prop.GetValue(b, null))) .Select(prop => prop.Name).ToArray(); } |
and of course, the unit test that goes along with it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[Test] public void GetDistinctProperties() { Expect(ReflectionUtils.GetDistinctProperties(null, null), Null); Expect(ReflectionUtils.GetDistinctProperties(new TestClass1(), null), Null); Expect(ReflectionUtils.GetDistinctProperties(null, new TestClass1()), Null); Expect(ReflectionUtils.GetDistinctProperties(new TestClass1(), new TestClass2()), Null); Expect(ReflectionUtils.GetDistinctProperties(new TestClass1(), new TestClass1()), EquivalentTo(new string[] {})); Expect(ReflectionUtils.GetDistinctProperties(new TestClass1() {Foo = "haha"}, new TestClass1()), EquivalentTo(new string[] {"Foo"})); Expect(ReflectionUtils.GetDistinctProperties(new TestClass1(), new TestClass1() {Bar = 10}), EquivalentTo(new string[] {"Bar"})); } [Test, ExpectedException(typeof(ArgumentException))] public void GetDistinctProperties_ThrowsWithIndexedProperty() { ReflectionUtils.GetDistinctProperties("haha", "jeje"); } |
It can then be used in a unit test like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[Test] public void BasicOperations() { RegionParameters parameters = new RegionParameters(5, 5, 5, 5); var tilemap = new BasicTileMap(parameters); var tile = new Tile() { MagmaLevel = 0, WaterLevel = 0, RockType = 1, State = TileState.HasFloor }; tilemap.Update(new Location(0, 0, 0), tile); var got = tilemap.Get(new Location(0, 0, 0)); Expect(ReflectionUtils.GetDistinctProperties(tile, got), Empty); } |
You can wrap the method via a unit-test friendly static assert method or any way you like. The above test would fail in a quite explicit way. An error message looks like this:
1 2 3 4 5 |
Expected: <empty> But was: < "MagmaLevel" > at NUnit.Framework.Assert.That(Object actual, IResolveConstraint expression, String message, Object[] args) at Undermine.Engine.Tests.TileMaps.BasicTileMapTests.BasicOperations() in BasicTileMapTests.cs: line 29 |
What do you think ?