- All Things Tested Equally
- Building Equality by Assertion
- Ensuring Adherence to the Contract for Equality
- Refactoring the Test
- Back to the Deck Test
- Final Notes
Ensuring Adherence to the Contract for Equality
Read the Javadoc that Sun provides for Object’s equals method. According to Sun, the equals method "implements an equivalence relation on non-null object references." The Javadoc then supplies a bulleted list of conditions or properties that must hold true for the equals method to be valid.
The first condition of this contract specifies that the equals method must be "reflexive: for any non-null reference value x, x.equals(x) should return true." Let’s add this criterion to our existing unit test (see Listing 7).
Listing 7 Testing reflexivity.
public void testEquality() { ... // reflexivity: assertTrue(card1.equals(card1)); }
If you run this test, it passes—and you should have figured that it would! Every once in a while, you won’t see negative feedback after writing a test. You do want to know before running JUnit against a new assertion whether or not it should pass. Surprises indicate that something is awry. In the case of the contract for equality, most of the conditions should hold true based on the current implementation built via TDD.
Let’s add another aspect of the equality contract to the test: symmetry. For non-null values x and y, x.equals(y) should return true if and only if y.equals(x) returns true. Listing 8 shows a simple, straightforward way to implement this in testEquality.
Listing 8 Testing symmetry.
// symmetry: assertTrue(card1.equals(card1copy)); assertTrue(card1copy.equals(card1));
This too passes. You could also combine both assertions into a complex conditional.
The transitivity property of equality says that if x.equals(y) and y.equals(z), then x.equals(z). The coding of a transitivity test involves a third object, as shown in Listing 9.
Listing 9 Testing transitivity.
// transitivity: assertTrue(card1copy.equals(card1copy2)); assertTrue(card1.equals(card1copy2));
We proved earlier that card1 is equal to card1copy, so we needn’t repeat that assertion here.
Consistency is a matter of demonstrating that the equals method returns consistent results with subsequent calls, provided that no state changes within either object being compared. Technically, there’s no way to prove consistency from an external test, since it requires infinite comparisons. But that doesn’t really matter: TDD is not about absolute proof, nor is it about exhaustive testing against all possible conditions, which is impossible to do in any large system.
Instead, TDD is about driving the design of a system through code-based specification. TDD is also about building confidence via tests. As long as the tests give you confidence to proceed, they’re sufficient. A defect that you let slip might destroy some of that confidence. Your appropriate reaction is to learn when to write more tests.
To demonstrate consistency in tests, we could write a loop that executes the equals method against the same two objects thousands of times. Or we could compare the objects twice. For good measure, I’ll even include a consistency check against two unequal objects, as shown in Listing 10.
Listing 10 Testing consistency.
// consistency assertTrue(card1.equals(card1copy)); assertFalse(card1.equals(card2));
I’m sure that sometime in the future I’ll design a class in which consistency is a real potential concern. I’ll probably write a more robust test then. Today, I’m satisfied with the consistency test as is.
The final property of equality: If you compare a non-null Card instance to null, equals should return false. That’s easy enough to test, as shown in Listing 11.
Listing 11 Comparing to null.
// comparisons to null: assertFalse(card1.equals(null));
This assertion causes JUnit to error out with a NullPointerException, indicating that we’ll have to fix our equals method:
@Override public boolean equals(Object object) { if (object == null) return false; if (object.getClass() != this.getClass()) return false; Card card = (Card)object; return this.getRank().equals(card.getRank()) "" this.getSuit().equals(card.getSuit()); }