Test your equals method

Một phần của tài liệu Manning JUnit recipes practical methods for program (Trang 57 - 64)

Problem

You want to test your implementation of equals().

Background

Surprisingly enough, even though a strong object-oriented design depends on implementing the equals() method correctly, many programmers implement this method incorrectly. If you want to take advantage of the rest of this book, you need to implement equals() correctly for a certain number of your classes: in par- ticular your Value Objects. Although you generally will not have occasion to com- pare instances of action- or processing-oriented classes, you will need to compare instances of Value Objects, which these processing-oriented classes use to receive input and provide output. To store these Value Objects in collections (List, Set, Map) requires implementing equals() and hashCode() appropriately.2 Doing so becomes all the more important when you want to write Object Tests, because in

2 See chapter 3 of Joshua Bloch, Effective Java: Programming Language Guide, Addison-Wesley, 2001; or Peter Haggar, Practical Java: Programming Language Guide, Addison-Wesley, 2000, the section entitled

“Objects and Equality.”

27 Test your equals method

those tests you assert the equality of Value Objects and primitive values a great deal of the time.

NOTE Value Objects—A Value Object represents a value, such as an Integer, Money, or a Timestamp. What makes a Value Object different from other objects is that even if you have 20 different objects that all represent the same value, you can use those objects interchangeably—that is, you can treat them equally. It does not matter whether they are different objects in memory: 5 is 5 is 5, so if you have three objects each representing the number 5, you can use one of them in place of another without changing the meaning of your program. You need to implement equals() to reflect the fact that, in spite of being different objects in memory, my $20 is the same as your $20.

The wrapper classes for the primitive types behave as Value Objects (Integer, Double, and so on). String also behaves this way. Many examples in this book use a class called Money, and Money objects are certainly Value Objects.

Note that if your object is not a Value Object, then there is likely no need to test for equals at all, so stop here. This is true of most objects outside your applica- tion’s domain model.

NOTE A quick review of equals()The contract of the method equals() is not complex, but it may be unfamiliar to programmers who did not study equivalence relations during their time in mathematics class, so a quick review is in order. The method equals() must satisfy three well-known properties: the reflexive, symmetric, and transitive properties, also known as RST by mathematics students trying to memorize the names.

■ The reflexive property says that an object is equal to itself.

■ The symmetric property says that if I am equal to you, then you are equal to me and the other way around.

■ The transitive property says that if I am equal to you and you are equal to that guy over there, then I am equal to that guy over there. To use the ancient saying: “Equals of equals are equal.”

Beyond these mathematical properties, the equals() method must be consistent: no matter how many times you call it on an object, equals() answers the same way as long as neither object being compared changes.

Finally, no object equals null.

With this in mind, you are ready to test your Value Objects for their implementa- tion of equals().

28 CHAPTER 2 Elementary tests

Recipe

If you were to try to write the various tests on your own, you would quickly find out that it is a considerable amount of work. If you want to see that work first before taking a shortcut, skip to the Discussion section of this recipe and then come back here. If you just want to get to the point, we simplify this recipe considerably by taking advantage of the good work of Mike Bowler. His open source package GSBase (http://gsbase.sourceforge.net) provides a number of utilities for writing JUnit tests, including something called the EqualsTester. This class runs a com- plete suite of tests on your Value Object to determine whether your implementa- tion of equals() satisfies all the necessary properties.

Listing 2.1 shows an example of using EqualsTester to test equals() for the Money class.

package junit.cookbook.common.test;

import junit.cookbook.util.Money;

import junit.framework.TestCase;

import com.gargoylesoftware.base.testing.EqualsTester;

public class MoneyTest extends TestCase { public void testEquals() {

Money a = new Money(100, 0);

Money b = new Money(100, 0);

Money c = new Money(50, 0);

Object d = null;

new EqualsTester(a, b, c, d);

} }

Let us explain a little more fully the parameters you need to pass to EqualsTester:

■ a is a control object against which the other three objects are to be compared.

■ b is a different object in memory than a but equal to a according to its value.

■ c is expected not to be equal to a.

■ If the class you are testing is final—cannot be subclassed—then d ought to be null; otherwise, d represents an object that looks equal to a but is not. By

“looks equal,” we mean (for example) that d has the same properties as a, but because d has additional properties through subclassing, it is not equal to a. Listing 2.1 Testing Money.equals() with EqualsTester

29 Test your equals method

Returning to our example, the test will fail if we set d to null while allowing sub- classes of Money. We must decide whether to declare the class Money to be final or change the test to the following:

public class MoneyTest extends TestCase { public void testEquals() {

Money a = new Money(100, 0);

Money b = new Money(100, 0);

Money c = new Money(50, 0);

Money d = new Money(100, 0) { // Trivial subclass };

new EqualsTester(a, b, c, d);

} }

EqualsTester provides a quick way to test Value Objects for equality, defined as

“having properties with the same respective values,” which is sufficient for the vast majority of business applications.

Discussion

To understand why we recommend using EqualsTester, let us develop the tests ourselves.

We start with the reflexive property and write this test:

public class MoneyEqualsTest extends TestCase { private Money a;

protected void setUp() { a = new Money(100, 0);

}

public void testReflexive() { assertEquals(a, a);

} }

The next property is the symmetric one. We need another Money object for that:

package junit.cookbook.common.test;

import junit.cookbook.util.Money;

import junit.framework.TestCase;

public class MoneyEqualsTest extends TestCase { private Money a;

private Money b;

protected void setUp() { a = new Money(100, 0);

30 CHAPTER 2 Elementary tests

b = new Money(100, 0);

}

public void testReflexive() { assertEquals(a, a);

assertEquals(b, b);

}

public void testSymmetric() { assertEquals(a, b);

assertEquals(b, a);

} }

You may be tempted to write testSymmetric() as a logical implication, doing something like this:

public void testSymmetric() {

assertTrue(!a.equals(b) || b.equals(a));

}

This assumes that you remember how to convert an implication into a logic state- ment involving only AND, OR, and NOT. Maybe you do, maybe you do not. The good news is it does not matter, because by writing the test as we did previously, the test fails only if either assertion fails. In other words, there is an implicit AND between those assertions, so that both the forward and reverse equalities are veri- fied. The direct approach works, so we are content with that. In the process, we added b to the reflexivity test, just to have another data point to help us be more confident in the code that passes these tests.

The next property to test is transitivity. This normally would require three objects, but it turns out that in the vast majority of cases, the reflexive and symmet- ric properties together are enough to guarantee transitivity. The exceptional case is pretty unusual, so rather than describe it here, we leave it to a separate chapter.

See essay B.2, “Strangeness and transitivity,” for details.

Now that we have verified that objects are equal when we expect them to be, we need to verify that objects are not equal when we expect them not to be. This requires introducing a third object different from the first two. We add a declaration for this new object as a field in our test case class, as well as the corresponding tests:

public class MoneyEqualsTest extends TestCase { private Money a;

private Money b;

private Money c;

protected void setUp() { a = new Money(100, 0);

TE AM FL Y

Team-Fly®

31 Test your equals method

b = new Money(100, 0);

c = new Money(200, 0);

}

public void testReflexive() { assertEquals(a, a);

assertEquals(b, b);

assertEquals(c, c);

}

public void testSymmetric() { assertEquals(a, b);

assertEquals(b, a);

assertFalse(a.equals(c));

assertFalse(c.equals(a));

} }

These tests say that c should equal itself (as should every object), but a should not equal c and the other way around. You may notice the use of assertFalse (a.equals(c)). There is no assertNotEquals() method in JUnit, although you can find it in some of the extension projects, such as JUnit-addons. Some mem- bers of the community have asked for it, but it is still not part of JUnit. It is easy enough to write, so if you find you prefer to have it, then we recommend imple- menting it yourself.

At this point we have checked the three main properties. For consistency, we need to check equality on a pair of objects a number of times. Because we can’t use an infinite loop here, we choose a nice high number of comparisons that gives us some confidence without making the tests needlessly slow:

public class MoneyEqualsTest extends TestCase { // No changes to the other tests

public void testConsistent() { for (int i = 0; i < 1000; i++) { assertEquals(a, b);

assertFalse(a.equals(c));

} } }

Even at 1,000 iterations, the test only lasted 0.05 seconds on a midrange laptop, so the test should be fast enough. The last thing to do is compare the objects to null:

public class MoneyEqualsTest extends TestCase { // No changes to the other tests

public void testNotEqualToNull() {

32 CHAPTER 2 Elementary tests

assertFalse(a.equals(null));

assertFalse(c.equals(null));

} }

We choose not to compare b with null because the other tests show that a and b are already equal. If you want to add the extra test, it does no harm, but we do not think it provides any value.

This covers all the basic properties of equals(). Although it is not an exhaustive test, it should detect defects in an implementation of equals() in any classes you might find in business applications programming. Examine the source of Equals- Tester to see what additional tests Mike Bowler decided were worth implementing.

NOTE In addition to testing equals(), the EqualsTester verifies that hash- Code() is consistent with equals(). This means that if two objects are equal, then their hash codes are also equal, but not necessarily the other way around. Without these extra tests, it is possible to put an object into a Map with one key object and not be able to retrieve it with another key object, even if the two key objects are equal. We say this from experience.

If you do not want to or need to write a suitable hashing algorithm, we recommend implementing hashCode() to throw an UnsupportedOper- ationException, which adheres to the basic contract of Object but fails loudly if you later try to use this object as a key in a Map.3 An alternative is to return a constant value, such as 0, but that reduces hashing to a linear search, which may cause a performance problem if done often enough.

Alternative

JUnit-addons also provides an equals tester, which it calls EqualsHashCodeTest- Case. The intent is the same, but the code is different. You subclass this test case class and override methods to return instances of your Value Objects. One method, createInstance(), returns the control object; the other method, create- NotEqualInstance(), returns objects that you expect not to be equal to the con- trol objects. You need to implement these methods to return new objects on each invocation, rather than create one instance and keep returning it. The test will, for example, invoke createInstance() twice and then compare the two objects using equals(). If you return the same object twice, the test fails, so you do not necessarily have to remember to do this correctly. Listing 2.2 shows how we tested Money using this technique.

3 Thanks go to Ilja Preuò for recommending this technique. It is consistent with the practice of adding a fail() statement to a new test until it has been fully implemented. Until then, it can only fail, rather than pass silently.

33 Test a method that returns nothing

package junit.cookbook.common.test;

import junit.cookbook.util.Money;

import junitx.extensions.EqualsHashCodeTestCase;

public class MoneyEqualsTestWithJUnitAddons extends EqualsHashCodeTestCase {

public MoneyEqualsTestWithJUnitAddons(String name) { super(name);

}

protected Object createInstance() throws Exception { return Money.dollars(100);

}

protected Object createNotEqualInstance() throws Exception { return Money.dollars(200);

} }

To summarize the main differences between the two utilities, you use EqualsTester within a single test, whereas you use EqualsHashCodeTestCase by subclassing. Also, EqualsTester tests for the “subclass equals” issue, whereas EqualsHashCodeTestCase does not. Otherwise, use whichever technique you prefer.

Related

■ B.2—Strangeness and transitivity

■ GSBase (http://gsbase.sourceforge.net)

■ JUnit-addons (http://junit-addons.sourceforge.net)

Một phần của tài liệu Manning JUnit recipes practical methods for program (Trang 57 - 64)

Tải bản đầy đủ (PDF)

(753 trang)