Testing That a Method Throws
Every test so far has checked a successful operation. We should also test that a method throws an exception when it is supposed to. For example, we documented get with a promise about invalid input:
/**
* ...
* @throws IndexOutOfBoundsException if index < 0 or index >= size().
*/
T get(int index);
Throwing IndexOutOfBoundsException for an out-of-range index is part of get’s contract, just like returning the right element. So it needs its own test.
The first thing you might try is to call the method with a bad index and see what happens:
@Test
public void getBelowRangeThrows() {
list.get(-1); // we expect this to throw
}
This test fails if get(-1) throws, which is not what we want. If get(-1) does throw, the test should pass. As it stands, the exception propagates out of the test method uncaught, and JUnit reports the test as failed.
We have to assert that the exception happens. To do that, we catch the exception ourselves, so we control what happens next. Here is the idiom the course uses to say “this call must throw”:
import static org.junit.jupiter.api.Assertions.fail;
@Test
public void getBelowRangeThrows() {
try {
list.get(-1);
fail("expected IndexOutOfBoundsException for index -1");
} catch (IndexOutOfBoundsException e) {
return;
}
}
Trace the two things that can happen:
-
get(-1)throws, as the contract promises. Control jumps immediately to thecatch, skipping thefailline entirely. There is nothing to do in thecatch, so wereturn, and the test passes. -
get(-1)does not throw the expected exception. Heregetdoes not meet its contract. Execution continues to the next line, which isfail(...). Thefailmethod is a JUnit assertion that always fails, with the message we give it. So the test fails, and the message says exactly what was expected.
Notice we catch IndexOutOfBoundsException specifically, not Exception. The contract names a particular exception. It does not just promise that some exception is thrown. Suppose a buggy get threw a NullPointerException instead. Our catch only handles IndexOutOfBoundsException, so the NullPointerException would not be caught. It would propagate out of the test and be reported as a failure. That is correct, because the method threw the wrong exception. If we had written catch (Exception e), that wrong exception would have been caught too, and the test would have passed a broken method.
Why not assertThrows?
JUnit provides a shorter assertion method, assertThrows, for testing exceptions. We do not use it, because its natural form requires a lambda expression:
assertThrows(IndexOutOfBoundsException.class, () -> list.get(-1));
The () -> ... is functional-style Java, and I am assuming you have not seen it. A typical introduction to programming course does not cover it.
The try / fail / catch idiom does the same job using only the ordinary control flow you already know, so it is what we use throughout. You are expected to use this approach in any tests you write for the course.