◆ Problem
You want like to use assertEquals() to compare two JavaBeans to one another, but the JavaBean class does not implement equals() in a manner consistent with a Value Object.
◆ Background
In spite of repeated suggestions to do so, many programmers do not implement equals() correctly for their Value Objects. This is bad news for JUnit practitio- ners, as so many of their tests use assertEquals() to verify the result of a given behavior. If you are adding tests to legacy code or third-party code that you can- not change, you might have no direct way to use assertEquals() on objects that, by all rights, are Value Objects. JavaBeans typically fall into this category.
If you are not able to use assertEquals() then you are forced to compare your expected values and the actual values on a property-by-property basis, making tests longer than they need to be. There must be a better way.
◆ Recipe
GSBase allows you to determine whether two Java objects appear to be equal.
Although not a perfect equality test, this facility makes it possible to at least approximate the equality of JavaBeans by automating the property-by-property comparison you would otherwise need to code by hand. You can find this feature in the class com.gargoylesoftware.base.testing.TestUtil with the methods appearsEqual(), assertAppearsEqual(), and assertAppearsNotEqual().
582 CHAPTER 15 GSBase
The first method, appearsEqual(), takes two Objects and compares them for apparent equality, returning true when they appear to be equal and false other- wise. The assertion methods simply wrap appearsEqual() in the appropriate man- ner, asserting either that the two objects appear equal or do not.
Listing 15.3 shows an example of using “appears equal” along with the Event- Catcher (see recipe 15.1). As a convenience, EventCatcher provides the method assertEventsAppearEquals() so that you do not need to ask for the actual events and make the comparison yourself.
package junit.cookbook.gsbase.test;
import java.awt.Container;
import java.awt.event.HierarchyEvent;
import java.util.Arrays;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import junit.framework.TestCase;
import com.gargoylesoftware.base.testing.EventCatcher;
public class ConfirmationDialogTest extends TestCase { public void testEvents() throws Exception { JOptionPane optionPane =
new JOptionPane(
"Are you sure?!",
JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_CANCEL_OPTION);
EventCatcher eventCatcher = new EventCatcher();
eventCatcher.listenTo(optionPane);
JFrame mainFrame = new JFrame("The Main Frame");
Container mainContentPane = mainFrame.getContentPane();
mainContentPane.add(optionPane);
HierarchyEvent expectedHierarchyEvent = new HierarchyEvent(
optionPane,
HierarchyEvent.HIERARCHY_CHANGED, optionPane,
mainContentPane,
HierarchyEvent.PARENT_CHANGED);
List expectedEvents = Arrays.asList(
new Object[] { expectedHierarchyEvent });
Listing 15.3 Verifying events with "appears equal"
The event we expect
583 Compare JavaBeans
using “appears equal”
eventCatcher.assertEventsAppearEquals(
expectedEvents);
} }
◆ Discussion
Although it is a handy construct, do not mistake “appears equal” for actual equal- ity. The “appears equal” algorithm is simple and reasonable, but even the rela- tively benign use of public fields can make a liar of it. The algorithm works as follows. Each object is inspected for any readable properties by scanning for meth- ods that take no parameters. The method appearsEqual() invokes each of these methods on the two objects to compare them, and it fails if they return different values. As usual with JUnit, if no assertion fails, then appearsEqual() declares that the two objects appear to be equal. For any reasonably coded JavaBean, this algo- rithm works very nicely, but if your objects contain public fields, rather than con- form to the JavaBean specification, then “appears equal” simply will not work at all for those objects.
NOTE An important detail about “appears equal”—The idea behind “appears equal”
is to find all the methods that could reasonably be property accessors, invoke them on each of the two objects in question, and then compare the return values for equality. As implemented in GSBase 2.0, appearsE- qual() invokes all no-parameter methods, rather than just ones that fol- low the JavaBean property-naming conventions of getX() or isX() for readable properties. This means that appearsEqual() invoke such methods as init() and clear(), which would cause problems for cer- tain objects, creating unintended side effects.
All we can say on the matter is that appearsEqual() is intended to be invoked with eventlike objects, which tend to be immutable bags of data, having little or no behavior. Mike designed and built this method to use on GUI events along with EventCatcher, so if you need to apply the idea to other objects, and appearsEqual() is invoking the wrong methods on those objects, then use appearsEqual() as a model to build your own version, and then submit it to GSBase as a patch. Mike will consider it.
It is possible, we suppose, for JavaBeans to appear equal through their public, readable properties, and yet have additional fields whose values differ.6 In this case, it is up to you to decide the correct semantics: because the outside world
6 Perhaps this is largely a theoretical concern. Real-life examples are welcome, but we expect they are rare.
Did we catch the expected events?
584 CHAPTER 15 GSBase
cannot tell the difference between the two objects—at least from a value perspec- tive—perhaps they ought to be treated as apparently equal. Perhaps not. That is a judgment call that you need to make, and often you will take one of two stances.
You might be conservative and not consider the two objects apparently equal until you are certain that it is the case, or you might instead plunge ahead, assume that they are apparently equal, and not worry about that assumption until it is proven false. Without considering this issue in context—in other words, without having to write a specific test—we cannot know what the answer is. The approach you take depends on the goals of the test.
◆ Related
■ 15.1—Verify GUI events with EventCatcher
585
JUnit-addons
This chapter covers
■ Testing Comparable classes
■ Collecting tests from within archives
■ Organizing test data and sharing test resources
■ Ensuring that shared test fixtures clean themselves up
■ Reporting the name of each test as it executes
586 CHAPTER 16 JUnit-addons
Vladimir Bossicard is a long-time member of the JUnit community, and one of its most vocal members as well. Perhaps the one trait that identifies him most easily within the community is his insistence that we not content ourselves with whatever feature set JUnit provides. If JUnit does not do something you need, then Vladimir simply tells you to build it yourself, and then share it with the world through open source. It is in this spirit that he has led the evolution of JUnit-addons: a collection of helper classes for JUnit. Visit the JUnit-addons site at http://junit-addons.source- forge.net. In this chapter we describe some problems that JUnit-addons solves.
In the first chapter of recipes we described how to test your implementation of the equals() method. Related to the concept of object equality is object order, which defines less-than and greater-than relationships on your objects. If you have classes that implement the Comparable interface, then you will want to use Compa- rabilityTestCase to verify your implementation of the compareTo() method. This test case class provides a simple, standard set of tests for compareTo() so you can avoid writing them for yourself—and possibly forgetting some part of the Compa- rable.compareTo() contract.
In chapter 3, “Organizing and Building JUnit Tests,” we describe various ways to automatically collect test case classes into a large test suite. These techniques have generally assumed that your tests are sitting on the file system, either as loose class files or as Java source files. JUnit-addons provides a number of SuiteBuilder classes, including an implementation that collects tests from Java archives and *.zip files. This chapter includes a recipe describing how to collect tests from within a
*.jar file.
In chapter 5, “Working with Test Data,” we describe how to manage test data with properties files. If you would like to use this technique without duplicating (yet again) the code to integrate the properties files into your tests, this chapter contains a recipe for managing your properties file-based test data with Property- Manager. If you need to go beyond properties files to manage shared test resources, we describe how to use the ResourceManager to tame the proliferation of global access to test resources, such as a database. If you use shared test resources, then you might have problems with those resources correctly cleaning up after themselves. JUnit has a minor flaw in its implementation of TestSetup (see recipe 5.10, “Set up your fixture once for the entire suite”) that JUnit-addons corrects with its version of this class. In this chapter we describe how to ensure that your shared test fixture cleans itself up, even when its test suite ends badly.
Finally, going beyond merely augmenting JUnit, Vladimir has built his own JUnit test runner. Among its additional features is an open test listener architecture that allows you to monitor test execution and generate customized test reports. To help
587 Test your class for compareTo()
you get started, we describe another technique for displaying the name of each test as it executes, using JUnit-addons Test Runner. You can adapt this technique to write test results in any format you need: XML, comma-delimited text, even to a database table for sophisticated report generation. Never one to accept things the way they are, Vladimir has provided a rich library of JUnit utilities with his JUnit- addons that make any JUnit practitioner’s job easier.