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; }
}

© 2026 Xygeni. All rights reserved