Friday, November 10, 2006

Testing equals and hashCode

This isn't a post about whether it is a good idea to implement equals and hashCode in all your classes, and if so how (check all fields, check some kind of identifier, check fields except the boring ones, etc).

No, I'm assuming that you have decided to implement equals and hashCode, either because you like working that way, or because you are using a package like Hibernate which encourages/requires it.

So now the question is: being good test-driven developers that we are, how do we write the tests for our equals and hashCode methods? Many of us have probably read the javadoc for Object#equals (the so-called equals contract), and started out
writing things like:

assertTrue(one.equals(two));
assertTrue(two.equals(one));
assertFalse(one.equals(null));

etc.

And that's about right. But it seems like this is a framework waiting to happen (well, framework is probably too grandiose a word for something which probably doesn't need to be more than a hundred or so lines of code and just affects tests for equals and hashCode, but hey, people have called things frameworks for less).

Are there any good ones out there in Apache commons or the other usual places? I've seen some really bad ones, but generally have just ended up writing them myself. I'm enclosing the one I'm currently using in both Mayfly and MIFOS.

The one thing it doesn't do super-well is test transitivity. You can give it a bunch of things which should all be equals to each other, and it tests that they all are, but it doesn't do any transitivity tests for not-equals. I think it is pretty clear how to fix that: instead of just passing in a bunch A of things equals to each other, pass in several bunches: A, B, and C. Each object within A should be equals to the others in A, but none of the ones in B and C. Likewise for B and C (the mathematically experienced of you will recognized these "bunches" as equivalence classes). In fact, I started to implement this today, and I got a bit hung up on whether it reads as nicely as what I have now. Somehow, passing in Object[][] { new Object[] { a1,a2}} just seemed like too many levels of [] and {} and such. I don't know if my concern is justified.


public static void assertAllEqual(Object[] objects) {
/**
* The point of checking each pair is to make sure that equals is
* transitive per the contract of {@link Object#equals(java.lang.Object)}.
*/
for (int i = 0; i < objects.length; i++) {
Assert.assertFalse(objects[i].equals(null));
for (int j = 0; j < objects.length; j++) {
assertIsEqual(objects[i], objects[j]);
}
}
}

public static void assertIsEqual(Object one, Object two) {
Assert.assertTrue(one.equals(two));
Assert.assertTrue(two.equals(one));
Assert.assertEquals(one.hashCode(), two.hashCode());
}

public static void assertIsNotEqual(Object one, Object two) {
assertReflexiveAndNull(one);
assertReflexiveAndNull(two);
Assert.assertFalse(one.equals(two));
Assert.assertFalse(two.equals(one));
}

public static void assertReflexiveAndNull(Object object) {
Assert.assertTrue(object.equals(object));
Assert.assertFalse(object.equals(null));
}

2 comments:

Anonymous said...

There is always the EqualsTester from http://gsbase.sourceforge.net/

Jim Kingdon said...

Thanks for the pointer to EqualsTester. To summarize, you give it four objects: two that are supposed to be equal, one of the same class which is supposed to not be equal, and one of a subclass which is supposed to not be equal.

That's not bad, but what if I have *three* objects, all of which are supposed to be equals? Or what if I want to test an object which isn't a subclass at all (throwing ClassCastException rather than returning false when compared with an unrelated class is a common practice, probably dubious)?

It does expect you to declare a class final if it has no subclasses, see Marin Fowler's Seal article for discussion on whether this is good/bad.

Still, it is one of the better ones I've seen out there. It does test hashCode, and calling it isn't too involved (no need to inherit from some kind of EqualsTestCase or the like).