◆ Problem
You want to verify that a method throws an expected exception under the appro- priate circumstances. You may also be looking for “the simplest way” to write such a test.
◆ Background
To understand how to implement this kind of test, you need to know how JUnit decides whether a test passes or fails. A test fails (with either a failure or an error) if an assertion fails or it throws an exception; otherwise, the test passes. In other words, if a test falls through—meaning it executes from beginning to end without exiting from the middle—then it passes. This is enough to infer the way you ought to write this kind of test: it should fail if the offending line of code does not throw an exception; it should catch only the expected exception type; and it should throw any other exceptions up into the JUnit framework.
57 Test throwing the right exception
◆ Recipe
The following code demonstrates the implementation pattern for writing tests for throwing an exception:
public void testConstructorDiesWithNull() throws Exception { try {
Fraction oneOverZero = new Fraction(1, 0);
fail("Created fraction 1/0! That's undefined!");
}
catch (IllegalArgumentException expected) {
assertEquals("denominator", expected.getMessage());
} }
Now that you have seen the pattern, we’ll describe it here in more detail:
1 Identify the code that might throw the exception and place it in a try block.
2 After invoking the method that might throw an exception, place a fail() statement to indicate “If we got here, then the exception we expected was not thrown.”
3 Add a catch block for the expected exception.
4 Inside the catch block for the expected exception, verify that the excep- tion object’s properties are the ones you expect, if desired.
5 Declare that the test method throws Exception. This makes the code more resistant to change. Someone may change the method under test so that it declares it might throw additional checked exceptions. That change likely does not affect your test, so it ought not to cause your test to stop compiling.
◆ Discussion
If the method under test throws an unexpected exception—something other than the exception for which you are testing—then JUnit reports an error, rather than a failure, because the test method throws that exception up into the JUnit frame- work. Recall that an error generally indicates a problem in the environment or with the test itself, rather than a problem in the production code. If the produc- tion code throws an unexpected exception, then indeed, perhaps there is some underlying problem that hinders the test’s normal execution.
Returning to the example, note the name we use for the exception identifier:
expected. This is important, as it is a clear message to the programmer that throw- ing this exception—or catching it, depending on your perspective—is a good
58 CHAPTER 2 Elementary tests
thing. Because exceptions are used to model aberrant code execution paths, we’re accustomed to seeing exceptions as a bad thing. This kind of test is an exception—pun intended.
In many cases, it is not necessary to make assertions about the expected (caught) exception. If asserting the correctness of the type of exception caught is sufficient to give you confidence in the correctness of the production code, then leave the exception handler empty. In this case, some programmers like to add a comment reading “Expected path.” We feel that naming the exception object expected communicates that fact effectively without the need for a comment. This is a matter of personal taste, so do whatever works for you.
An alternative is to catch all exceptions that the method could possibly throw, adding a failure statement for each exception you do not expect. Often, when a programmer wants to do this, it is to report unexpected exceptions as failures, rather than as errors. We recommend against doing this for two simple reasons:
■ Doing so requires extra code compared to simply throwing the unexpected exceptions up into the JUnit framework, and all things being equal, less code is easier to maintain.
■ There is little to gain by reporting the unexpected exception as a failure. In many cases, you handle the two the same way. Some unexpected exceptions are production code problems and some are environment problems, and it is usually difficult to distinguish them without depending strongly on the underlying object’s implementation details.
A more object-oriented approach
Just when it looked like there was no more to write on this subject, an esteemed member of the JUnit community, Ilja Preuò (IL-ya PROYSS), presented a more object-oriented option for this code:
public void testForException() {
assertThrows(MyException.class, new ExceptionalClosure() { public Object execute(Object input) throws Exception { return doSomethingThatShouldThrowMyException();
} });
}
Although some find Java’s anonymous inner class syntax a little difficult to read, the method’s intent could not be more clear: “Assert that this block of code throws that kind of exception.” Implement the method assertThrows() as follows:
59 Test throwing the right exception
public static void assertThrows(
Class expectedExceptionClass, ExceptionalClosure closure) { String expectedExceptionClassName = expectedExceptionClass.getName();
try {
closure.execute(null);
fail(
"Block did not throw an exception of type "
+ expectedExceptionClassName);
}
catch (Exception e) { assertTrue(
"Caught exception of type <"
+ e.getClass().getName() + ">, expected one of type <"
+ expectedExceptionClassName + ">",
expectedExceptionClass.isInstance(e));
} }
Rather than catching only the exception we expect, we need to catch them all, because we cannot know at compile time which exception class to expect. When making an assertion generic like this one, a good failure message is important, because you are removing control of the failure message from the invoker. The alternative is to add another parameter to assertThrows() that accepts a custom failure message. Finally, because we must catch all exceptions, we are forced to test the caught exception class against our expectation. The method Class.
isInstance(Object) answers whether the parameter is an instance of the class. It is the same as using instanceof.
In addition to revealing intent, this approach avoids duplicating the exception- checking algorithm in hundreds of tests. That’s quite an improvement!
You will notice the use of ExceptionalClosure, which requires some introduc- tion. A closure is simply a wrapper for a block of code, as is directly supported in languages like Smalltalk and Ruby. Rather than reinvent the wheel, we usually use the Closure interface from the Jakarta Commons project (http://jakarta. apache.
org) as often as we can. Unfortunately for this recipe, we cannot use it because its execute() method does not declare that it might throw a checked exception, so it can only throw an unchecked one—that is, a descendant of RuntimeException. To compensate for this deficiency, we added the interface ExceptionalClosure to Diasparsoft Toolkit, which does what Closure does, but whose execute() method might throw an exception.
Forced to do this
Detailed failure message Verify the caught exception class
60 CHAPTER 2 Elementary tests
Once you have implemented assertThrows() once, you can move it up into your own customized assertion superclass and use it whenever you want. See rec- ipe 17.4, “Extract a custom assertion,” for a discussion of customized assertions.
Watch your assertions
Be careful with your assertions about the exception you expect to throw: if you verify the exception object too closely, you may introduce overly strong coupling between the tests and the production code. The resulting test is more brittle than it needs to be.12 Consider what happens if the exception message is meant to be displayed to an end user, and you write an assertion that checks that message directly. Typically, you will write a test that resembles the following:
public void testNameNotEntered() { try {
login("", "password");
fail("User logged in without a user name!");
}
catch (MissingEntryException expected) {
assertEquals("userName", expected.getEntryName());
assertEquals(
"Please enter a user name and try again.", expected.getMessage());
} }
This test is clear: if a user attempts to log in without providing a user name, then login throws a MissingEntryException including both a name for the missing entry and a message suitable to present to the end user. This appears to be good cohesion: the exception object contains simplified exception data for you to use as well as a user-readable message. This looks like everything that the object could need.
While the first assertion in the catch block is a good idea, we have reservations about the second. Although the name userName is internal to the program and part of its behavior, the user-readable message could change at any time without affecting the way the login feature is implemented. In short, if userName changes, then the test likely needs to change; however, the test likely does not need to change in response to a change in the user-readable message. As the test is written now, a change to either requires a change in the test.
In this case, we recommend removing the second assertion and keeping the first. It is a general design goal to keep user-readable messages separate from the
12For example, you may rewrite assertThrows(), changing the “expected exception class” parameter to an “expected exception object” parameter, which is a stronger assertion—perhaps too strong.
TE AM FL Y
Team-Fly®
61 Let collections compare themselves
internals of an object’s behavior. You can always test an exception message by sim- ply instantiating the exception and checking the return value of toString() or getMessage(). There is no need to actually throw the exception to test this aspect of the exception class’s behavior.
◆ Related
■ 17.4—Extract a custom assertion
■ Jeff Langr, Essential Java Style: Patterns for Implementation, Prentice Hall PTR, 1999
■ Jakarta Commons project (http://jakarta.apache.org)