Skip to content

异常和超时测试:确保代码按预期失败

好的测试不仅要验证正常路径,也要验证异常路径。

一个不抛异常的 NPE、一个应该超时的方法没超时——这些往往是生产环境的定时炸弹。

异常测试:确保该抛的时候抛了

assertThrows:断言抛出指定异常

java
@Test
void testThrowsException() {
    // 断言抛出 ArithmeticException
    ArithmeticException ex = assertThrows(ArithmeticException.class, () -> {
        int result = 10 / 0;
    });
    // 可以进一步验证异常信息
    assertEquals("/ by zero", ex.getMessage());
}

验证异常消息

java
@Test
void testExceptionMessage() {
    Exception ex = assertThrows(RuntimeException.class, () -> {
        throw new RuntimeException("用户不存在: 12345");
    });

    assertThat(ex.getMessage())
        .contains("12345")
        .startsWith("用户不存在");
}

断言不抛异常

java
@Test
void testDoesNotThrow() {
    assertDoesNotThrow(() -> {
        int result = 10 / 2; // 不会抛异常
    });
}

超时测试:防止死循环和性能退化

@Timeout 注解

java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import java.util.concurrent.TimeUnit;

class TimeoutDemo {

    @Test
    @Timeout(value = 1, unit = TimeUnit.SECONDS)  // 1 秒内必须完成
    void testWithTimeout() {
        // 如果这个方法执行超过 1 秒,测试自动失败
        doSomething();
    }
}

assertTimeoutPreemptively:抢占式超时

java
@Test
void testTimeoutAssertion() {
    assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
        // 抢占式超时:超过 100ms 强制中断线程
        Thread.sleep(50); // 不会中断,50ms < 100ms
    });
}

assertTimeoutPreemptively@Timeout 都是抢占式的——超时后强制中断线程。相比之下 assertTimeout 是计时型的,只记录是否超时,不会中断。

超时陷阱

java
// ❌ 如果测试代码包含对外部服务的真实调用,100ms 肯定不够
@Test
@Timeout(1)
void testExternalCall() {
    httpClient.call("http://slow-api.com"); // 假设这个 API 响应需要 3 秒
}

// ✅ Mock 外部依赖,让测试纯粹测自己的逻辑
@Test
@Timeout(1)
void testExternalCall() {
    when(httpClient.call(anyString())).thenReturn("mocked");
    // 测试代码逻辑
}

组合场景

异常 + 超时

java
@Test
@Timeout(value = 2, unit = TimeUnit.SECONDS)
void testWithExceptionAndTimeout() {
    assertThrows(IllegalArgumentException.class, () -> {
        throw new IllegalArgumentException("Invalid input");
    });
}

常见误区

1. 只测正常路径

java
// ❌ 只测成功的情况
@Test
void testDivide() {
    assertEquals(5, calculator.divide(10, 2));
}

// ✅ 还要测异常路径
@Test
void testDivideByZero() {
    assertThrows(ArithmeticException.class, () -> {
        calculator.divide(10, 0);
    });
}

2. 超时设置过长

超时太宽松就失去了意义。应该根据实际业务逻辑的最长时间来设置。

3. 假设超时是精确的

超时断言的精度受 JVM 调度影响,不建议用超时代替性能测试。

总结

异常测试和超时测试是测试体系里常被忽视的两个角落:

  • 异常测试:确保代码在遇到错误时正确处理(该抛的抛,该捕获的捕获)
  • 超时测试:防止代码陷入死循环或性能严重退化

好的测试套件,既要确保正常路径走通,也要确保异常路径走对。

基于 VitePress 构建