Ambiguous Expected-Exception Try/Catch
ID |
java.one_method_invocation_expected_testing |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Reliability |
Language |
Java |
Tags |
reliability, testing |
Description
Reports the manual "expected exception" idiom — a try block that ends by calling fail(…)
if the code under test did not throw, followed by a catch of the expected exception — when
the try body calls two or more different methods that can each raise the caught checked
exception. Nothing in the test says which call was supposed to be the one that throws.
Rationale
The whole point of this idiom is that reaching fail(…) proves the exception was not
thrown, so seeing it caught instead proves it was. That proof only means something if there is
exactly one candidate for what raised it. When a second call in the same block can raise the same
checked exception, the test still passes if either one throws for the wrong reason — a
regression in the call that was never meant to fail slips through disguised as the pass the test
was written to check for.
class OrderServiceTest {
@org.junit.jupiter.api.Test
void rejectsClosedAccount() {
try {
Account account = loadAccount(42); // also declares "throws AccountException"
orderService.place(account, cart); // declares "throws AccountException"
org.junit.jupiter.api.Assertions.fail("Expected an AccountException");
} catch (AccountException expected) { // FLAW -- which call actually raised it?
// ...
}
}
Account loadAccount(int id) throws AccountException { /* ... */ return null; }
}
Either narrow the try to the single call under test, or capture the setup call’s result outside
the block entirely so only the call being verified remains inside it.
Remediation
Move every call except the one under test outside the try block, capturing whatever result it
returns in a local variable the try block then uses. Only the call whose exception behavior the
test actually verifies should be able to raise the caught type from inside the block.
class OrderServiceTest {
@org.junit.jupiter.api.Test
void rejectsClosedAccount() throws AccountException {
Account account = loadAccount(42);
try {
orderService.place(account, cart); // only this call is under test
org.junit.jupiter.api.Assertions.fail("Expected an AccountException");
} catch (AccountException expected) {
// ...
}
}
Account loadAccount(int id) throws AccountException { /* ... */ return null; }
}