一、 为何异常测试不可或缺?
在理想的流程中,代码平稳运行。但现实世界充满意外:用户输入错误、网络中断、文件丢失……健壮的程序必须能预见并妥善处理这些异常。单元测试的核心目标之一就是验证代码不仅在“阳光明媚”时正确,更在“风雨交加”(异常)时表现符合预期。忽略异常测试,等同于为项目埋下未知的地雷。
二、 从传统到现代:异常测试方法的演进
1. 陈旧且不推荐的方式:try-catch 与 @Test(expected)
try-catch 块:在测试方法中手动编写try-catch,在catch中断言捕获到的异常,若未捕获则fail()。此法繁琐且容易遗漏fail(),导致假阳性(未抛异常却通过了测试)
@Test
public void testExceptionOldWay() {
try {
someMethodThatShouldThrow();
fail("Expected an IllegalArgumentException to be thrown");
} catch (IllegalArgumentException e) {
assertThat(e.getMessage()).contains("invalid");
}
}
JUnit 4 的 @Test(expected):只能验证异常类型,无法对异常消息、原因等属性进行断言,能力过于薄弱
@Test(expected = IllegalArgumentException.class)
public void testExceptionWithExpected() {
someMethodThatShouldThrow();
}
2. 现代最佳实践:JUnit 5 assertThrows() 与 AssertJ
JUnit 5的 Assertions.assertThrows() 和强大的断言库AssertJ提供了异常测试的终极解决方案。
三、 深度实战:现代异常测试示例
假设我们有一个简单的Calculator类,其中有一个divide方法。
public class Calculator {
public double divide(int dividend, int divisor) {
if (divisor == 0) {
throw new IllegalArgumentException("Divisor cannot be zero");
}
return (double) dividend / divisor;
}
}
示例1:JUnit 5 assertThrows() 基础用法
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void divide_ByZero_ThrowsIllegalArgumentException() {
Calculator calculator = new Calculator();
// 使用 assertThrows 捕获异常,并返回该异常实例以供进一步断言
Exception exception = assertThrows(
IllegalArgumentException.class, // 期望的异常类型
() -> calculator.divide(10, 0) // 执行待测试方法的Lambda表达式
);
// 对异常消息进行精确断言
assertEquals("Divisor cannot be zero", exception.getMessage());
}
}
示例2:使用AssertJ进行更优雅、强大的断言
AssertJ的流畅API让异常断言变得异常清晰和强大。
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
class CalculatorTestWithAssertJ {
@Test
void divide_ByZero_ThrowsIllegalArgumentExceptionWithMessage() {
Calculator calculator = new Calculator();
// 写法一:使用 assertThatThrownBy (非常直观)
assertThatThrownBy(() -> calculator.divide(10, 0))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Divisor cannot be zero"); // 还可以检查消息包含、模式匹配等
// 写法二:使用 assertThatExceptionOfType (BDD风格)
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> calculator.divide(10, 0))
.withMessage("Divisor cannot be zero");
}
}
示例3:验证检查异常(Checked Exceptions)
对于Checked Exception(如IOException),方法签名本身就会强制处理,测试思路完全一致。
public class FileProcessor {
public String readFirstLine(String path) throws IOException {
// ... 文件操作
if (!Files.exists(Paths.get(path))) {
throw new IOException("File not found: " + path);
}
// ...
}
}
@Test
void readFirstLine_FileNotFound_ThrowsIOException() {
FileProcessor processor = new FileProcessor();
// JUnit 5
IOException e = assertThrows(IOException.class, () -> processor.readFirstLine("nonexistent.txt"));
assertTrue(e.getMessage().contains("File not found"));
// AssertJ
assertThatThrownBy(() -> processor.readFirstLine("nonexistent.txt"))
.isInstanceOf(IOException.class)
.hasMessageContaining("File not found");
}
示例4:参数化测试异常场景
使用JUnit 5的@ParameterizedTest可以极简地测试多种会引发异常的输入。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@ParameterizedTest
@ValueSource(ints = {0, -0}) // 提供多个会导致除零的测试参数
void divide_ByVariousZeroValues_ThrowsException(int divisor) {
Calculator calculator = new Calculator();
assertThrows(IllegalArgumentException.class, () -> calculator.divide(10, divisor));
}
四、 总结与最佳实践
- 摒弃旧习:停止使用
@Test(expected)和手写try-catch。 - 首选JUnit 5 + AssertJ:
assertThrows()用于捕获,结合AssertJ进行强大、可读性高的断言。 - 断言要充分:不仅要验证异常类型,还要验证异常消息、原因(
getCause())、特定属性等,确保是“正确的”异常。 - 利用参数化测试:高效覆盖多种异常输入场景。
- 保持测试隔离:确保测试方法互不影响,一个测试只关注一种异常情况。
通过 mastering 这些现代异常测试技术,你将能构建出真正可靠、易于维护的测试套件,牢牢守住代码质量的最后一道关卡。
1308

被折叠的 条评论
为什么被折叠?



