As we showed in Chapter 8, the object-oriented testing frameworks make it easy to verify that a particular type of exception is thrown. Oftentimes, this is sufficient to determine the correctness of our code. Many methods only throw a single exception or only have a single error handling code path that requires testing.
The code in Listing 11-1 typifies a unified exception handling strategy in which errors are wrapped in an application-specific exception tailored to the context.
public class SomeClass {
public void doSomething() throws ServiceFailureException {
try {
makeNetworkRequest();
} catch (RemoteException rexp) {
throw new ServiceFailureException (rexp);
}
}
}
All RemoteException
s thrown by makeNetworkRequest()
will be wrapped in an exception specific to the context: in this case, the trite act of “doing something.” You could convincingly argue that the value added by the error handling in this method is simply the wrapping and that the particular derived RemoteException
is irrelevant to the operation of the method. In that case, verifying that a ServiceFailureException
is thrown may be sufficient if makeNetworkRequest()
cannot also throw that exception. The JUnit test for this code is in Listing 11-2.
@Test(expected = ServiceFailureException.class)
public void testDoSomething_Exception() {
// Create fixture
sut.doSomething();
}
The expected
attribute of the @Test
annotation tells JUnit to only consider the test as passed if an exception of the specified type is thrown. If another type of exception is thrown, or no exception is thrown, JUnit fails the test.
Using annotations greatly simplifies writing error condition tests. If you are using a language or framework without equivalent functionality, you may need to write the equivalent behavior yourself. Although the pattern is straightforward, some details are commonly missed when implementing it for the first time. Listing 11-3 demonstrates correct exception testing without direct framework support.
public void testDoSomething_Exception() {
// Create fixture
try {
sut.doSomething();
fail();
} catch(ServiceFailureException expected) {
// Expected behavior
}
}
We still rely on framework behavior to fail if any exception other than ServiceFailureException
is thrown. If that functionality is not present, then use another catch
clause with a fail()
assertion.
The included catch
clause deliberately ignores the exception we expect. The comment in the clause clearly communicates that this is the expected result; the comment will also suffice for many static checkers so they do not complain about an empty code block, something that I deem acceptable only in a testing context.
The key point that inexperienced testers often miss is the use of the fail()
assertion in the try
block. What would happen if the method accidentally succeeded without throwing an exception? Control would flow past it, out of the try
block, past the catch
block, and out of the method without encountering an assertion, resulting in a passed test. The additional assertion prevents a false positive result.
13.59.209.131