◆ Problem
You want to test a method that has no return value.
◆ Background
One of the most common questions for people new to JUnit is, “How do I test a method that returns void?” The primary reason for this question is that there is no
Listing 2.2 Testing Money.equals() with EqualsHashCodeTestCase
34 CHAPTER 2 Elementary tests
direct way to determine what such a method does. Because you cannot compare an expected value against an actual, returned value, you need some other way to describe the expected behavior and compare it to the actual behavior.
◆ Recipe
If a method returns no value, it must have some observable side effect, such as changing the state of an object; otherwise it does nothing. If it does nothing, then there is no need to test it and likely no need to use it, so get rid of it. If it has an observable side effect, you need to identify it and then make the appropriate assertions based on that side effect.
Let us test adding an object to a collection. The method Collection.
add(Object) returns no value. In spite of this, there is an intuitive way to verify whether the object was successfully added to the collection:
1 Create an empty collection.
2 The collection should not contain the item in question.
3 Add the item in question.
4 Now the collection should contain the item in question.
Translating that into code, we test ArrayList.add():
public class AddToArrayListTest extends TestCase { public void testListAdd() {
List list = new ArrayList();
assertFalse(list.contains("hello"));
list.add("hello");
assertTrue(list.contains("hello"));
} }
We saw that List.contains(Object) allows us to confirm a side effect of the cor- rect behavior of add(). Even though add() returns no value, we were able to iden- tify a change in list’s state that implies that add() behaves correctly.
We can apply the same technique to a method that loads data from some source but does not explicitly signal that that data was successfully loaded.4 This test loads data from a properties file and then makes some assertions about the prop- erties we expect to find:
4 Perhaps it should signal that information through a return value, because that would be useful to client objects other than the tests.
35 Test a method that returns nothing
public void testLoadProperties() throws Exception { Properties properties = new Properties();
properties.load(new FileInputStream("application.properties"));
assertEquals("jbrains", properties.getProperty("username"));
assertEquals("jbra1ns", properties.getProperty("password"));
}
Notice that this test is somewhat brittle because it duplicates expected values with data found outside the test, namely in a file on the file system. We address this problem in recipe 5.3, “Use an inline data file.” Still, the test uses the load() method’s key side effect to verify its behavior: that the expected data is correctly loaded from the specified properties file. (And, no, I don’t really use that pass- word for anything, so don’t bother trying it.)
◆ Discussion
The effectiveness of this technique relies on the existence of an observable side effect. Any method—any block of code, for that matter—must either return a value or have a side effect; otherwise, by definition it has no behavior. The only remaining question is whether the current design allows you to see the side effect.
A cache generally does not provide a way to view its contents, since caching is usu- ally an implementation detail. Without adding diagnostic methods to query whether the cache has been hit, there is no way to observe the behavior of a cach- ing mechanism. Of course, if your cache does not provide some means of verify- ing cache hits, how do you know that it is caching anything?5
Even with an observable side effect, you may question the value of using one method to test another. Returning to the List example, we used contains() to test add(), but typically, add() is used to test contains(). The test in question is generally the exact test we wrote previously to verify add()! There appears to be a chicken-and-egg problem here: we cannot guarantee the correctness of add() without having already demonstrated the correctness of contains(), and the other way around. Does this mean that our tests mean nothing?
The answer is no. We are testing behavior, and not methods. When we say “We will test adding objects to a collection,” that leads to testing a number of different operations: add, clear, remove, contains, find, and so on. All these operations on a collection pertain to testing the ability to add objects. Taken together, the meth-
5 Imagine a programmer codes his intention to cache an object by naming a method getObjectFrom- Cache(). In his haste to complete his programming task, he defers implementing the cache until later.
Three months later, in a formal code walkthrough, someone finally notices that there is no cache in this class. A test would have prevented this.
36 CHAPTER 2 Elementary tests
ods implement certain behaviors, such as the ability to have many objects in a col- lection or eliminate duplicate elements (for a set, in particular). We recommend you worry about the bigger picture—testing behavior—rather than testing indi- vidual methods. Certainly some behavior is straightforward and best implemented by a single method, but it is the object that we care about, and not just the method. If we need to invoke add(), contains(), and clear() to verify that our collection properly sorts elements as they are added to the list, then we invoke them. We will have more to say on this issue throughout the book.
NOTE The tests are the specification!—If add() and contains() contain defects that cancel one another out, then the test passes and we can argue that the behavior is correct. We hope that future tests will uncover the defects, but if that never happens then we have incorrect code that exhibits cor- rect behavior. If code does the wrong thing but no test fails, does it have a defect?
More and more JUnit users say, “No.” They say that “The tests are the specification,” which means that we describe what our code does by pro- viding the tests our code passes. A feature is not present unless there are tests to verify it. Each test is a claim that the code behaves a certain way, so if no test exists to verify that we can add an object twice to the same List, then we cannot assume that that would work. This certainly puts pressure on the programmer to write enough tests!
If there is no way to observe a method’s side effect, then in order to test it you must expose one.6 Although you may cringe at the notion of adding behavior “just for test- ing,” we argue that the return on investment far outweighs the cost of adding a simple query method, so long as it is, indeed, simple. In most cases, converting an invisible side effect into an observable one provides additional design benefits beyond “mere”
testability. One example is allowing a reusable class to emerge from a larger class in which it is currently trapped. As you gain more experience with JUnit, you will dis- cover this for yourself. We will also return to this point as needed throughout the book.
◆ Related
■ 5.3—Use an inline data file
■ 14.2—Test an Observable (Event Source)
■ B.4—The mock objects landscape
6 Jason Menard points out that you can use reflection to create an observable side effect by gaining access to otherwise invisible data or methods. We prefer not to do this, because it is often more work than just adding a method. But it is a viable alternative. We describe using JUnitX for this purpose in chapter 17.
37 Test a constructor