1. 引言

本文将深入探讨如何使用JUnit库测试代码是否抛出异常,以及如何验证代码未抛出任何异常。我们将同时覆盖JUnit 4和JUnit 5两个版本的核心方法。

2. JUnit 5异常处理

2.1. 断言异常抛出

JUnit 5 Jupiter断言API引入了assertThrows方法专门用于异常断言。该方法接收两个参数:

  • 预期的异常类型
  • 一个Executable函数式接口(通过Lambda表达式包裹待测代码)
@Test
void whenExceptionThrown_thenAssertionSucceeds() {
    Exception exception = assertThrows(NumberFormatException.class, () -> {
        Integer.parseInt("1a");
    });

    String expectedMessage = "For input string";
    String actualMessage = exception.getMessage();

    assertTrue(actualMessage.contains(expectedMessage));
}

核心要点: ✅ 当预期异常抛出时,assertThrows会返回异常实例,便于进一步验证异常信息 ✅ 该断言满足多态匹配:当抛出NumberFormatException或其子类时断言均会通过 ⚠️ 若将预期类型设为Exception,则任何异常都会使断言通过(因为Exception是所有异常的父类)

多态匹配示例:

@Test
void whenDerivedExceptionThrown_thenAssertionSucceeds() {
    Exception exception = assertThrows(RuntimeException.class, () -> {
        Integer.parseInt("1a");  // 实际抛出NumberFormatException
    });

    String expectedMessage = "For input string";
    String actualMessage = exception.getMessage();

    assertTrue(actualMessage.contains(expectedMessage));
}

2.2. 断言无异常抛出

使用assertDoesNotThrow()验证代码块执行时不抛出异常:

@Test
void givenABlock_whenExecutes_thenEnsureNoExceptionThrown() {
    assertDoesNotThrow(() -> {
        Integer.parseInt("100");  // 正常执行
    });
}

执行逻辑:

  • 代码块无异常 → 测试通过
  • 代码块抛出异常 → 测试失败

2.3. 断言特定类型异常未抛出

JUnit未内置此功能,需自定义实现。首先定义函数式接口:

@FunctionalInterface
public interface Executable {
    void execute() throws Exception;
}

然后实现自定义断言方法:

private <T extends Exception> void assertSpecificExceptionIsNotThrown(Class<T> exceptionClass, Executable executable) {
    try {
        executable.execute();
    } catch (Exception e) {
        if (exceptionClass.isInstance(e)) {
            fail(e.getClass().getSimpleName() + " was thrown");
        } else {
            // 其他异常类型被忽略,测试通过!
            // 记录日志便于调试
            LOG.info("Caught exception: " + e.getClass().getName() + ", but ignoring since it is not an instance of " + exceptionClass.getName());
        }
    }
}

使用示例:

@Test
void givenASpecificExceptionType_whenBlockExecutes_thenEnsureThatExceptionIsNotThrown() {
    assertSpecificExceptionIsNotThrown(IllegalArgumentException.class, () -> {
        int i = 100 / 0;  // 实际抛出ArithmeticException
    });
}

行为说明:

  • 抛出IllegalArgumentException或其子类 → 测试失败
  • 抛出其他异常或无异常 → 测试通过

3. JUnit 4异常处理

3.1. 断言异常抛出

基础方案: 使用@Test注解的expected属性

@Test(expected = NullPointerException.class)
public void whenExceptionThrown_thenExpectationSatisfied() {
    String test = null;
    test.length();  // 触发NullPointerException
}

进阶方案: 使用ExpectedException规则验证异常属性

@Rule
public ExpectedException exceptionRule = ExpectedException.none();

@Test
public void whenExceptionThrown_thenRuleIsApplied() {
    exceptionRule.expect(NumberFormatException.class);
    exceptionRule.expectMessage("For input string");
    Integer.parseInt("1a");  // 触发异常
}

对比说明: | 方案 | 优点 | 局限性 | |------|------|--------| | expected属性 | 简单直接 | 无法验证异常信息 | | ExpectedException规则 | 可验证异常类型/消息/原因 | 需要额外声明Rule |

3.2. 断言无异常抛出

JUnit 4无内置方法,需手动实现:

private void assertNoExceptionIsThrown(Executable executable) {
    try {
        executable.execute();
    } catch (Exception e) {
        fail(e.getClass().getSimpleName() + " was thrown");
    }
}

使用示例:

@Test
public void givenABlock_whenExecuted_thenEnsureThatNoExceptionAreThrown() {
    assertNoExceptionIsThrown(() -> {
        int d = 100 / 10;  // 正常执行
    });
}

执行逻辑:

  • 代码块无异常 → 测试通过
  • 代码块抛出任何异常 → 测试失败

4. 总结

本文系统梳理了JUnit 4和5中的异常断言方案:

JUnit 5优势:

  • assertThrows支持Lambda表达式,定位更精准
  • ✅ 内置assertDoesNotThrow方法
  • ✅ 异常返回值便于深度验证

JUnit 4特点:

  • @Test(expected)简单易用
  • ExpectedException规则提供灵活验证
  • ❌ 无内置"无异常"断言方法

最佳实践建议:

  1. 优先使用JUnit 5的现代API
  2. 需要验证异常细节时,避免使用@Test(expected)
  3. 自定义断言方法时注意异常类型的多态特性

完整示例代码请查阅:GitHub仓库


原始标题:Assert an Exception Is Thrown in JUnit 4 and 5 | Baeldung