
在软件开发中,单元测试是确保代码质量的关键环节。当我们的方法在特定条件下预期会抛出异常时,如何有效地测试这些异常行为成为了一个重要课题。assertj作为一个功能强大的断言库,提供了多种机制来处理这种情况。
1. 常见误区:直接断言方法返回值
许多开发者在初次尝试测试异常时,可能会直观地尝试对方法调用的结果进行断言,以检查是否抛出了特定类型的异常。以下是一个常见的错误示例:
假设我们有一个方法enterTheAmount,它接受用户输入的金额,并检查该金额是否为指定价格的倍数。如果不是,则抛出IllegalArgumentException。
public class Application {
public static int enterTheAmount(){
final int LOTTO_PRICE = 1000;
// 模拟从控制台读取输入,这里简化为直接解析字符串
// 在实际测试中,通常会通过System.setIn()重定向输入流
int amount = Integer.parseInt(Console.readLine()); // 假设Console.readLine()已实现
if(amount % LOTTO_PRICE != 0) {
throw new IllegalArgumentException("金额必须是" + LOTTO_PRICE + "的倍数。");
}
return amount / LOTTO_PRICE;
}
}为了测试当输入无效金额时是否抛出IllegalArgumentException,可能会有如下错误的测试尝试:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
class ApplicationTest {
// 模拟Console.readLine()所需的输入流重定向
private void setSystemIn(String input) {
InputStream in = new ByteArrayInputStream(input.getBytes());
System.setIn(in);
}
@Test
void validateTheEnteredAmount_incorrectly() {
final String INVALID_NUMBER = "1234";
setSystemIn(INVALID_NUMBER); // 设置模拟输入
// 错误的断言方式:试图对方法的返回值进行isInstanceOf判断
// 这段代码会失败,因为异常会在assertThat()被调用之前抛出
// 导致测试框架捕获到一个未处理的异常,而不是AssertJ的断言失败
assertThat(Application.enterTheAmount())
.isInstanceOf(IllegalArgumentException.class);
}
}问题分析:
上述测试代码的根本问题在于,Application.enterTheAmount()方法在遇到无效输入(如"1234")时会立即抛出IllegalArgumentException。这意味着assertThat()方法根本无法接收到任何返回值来执行isInstanceOf()断言。异常会在方法调用时直接中断程序的正常流程,并被JUnit测试框架捕获为未处理的异常,从而导致测试失败,而不是我们期望的AssertJ断言失败。
2. 正确姿势:使用 assertThrows 验证异常
AssertJ以及JUnit 5(通过assertThrows)提供了专门用于测试异常的方法,能够优雅且准确地验证预期异常。AssertJ的assertThrows方法(实际上是Assertions.assertThatThrownBy或JUnit 5的assertThrows)是解决此类问题的正确途径。这里我们主要介绍JUnit 5的assertThrows,因为它在现代Java测试中更为常用且语义清晰。
assertThrows方法接受两个参数:预期的异常类型和一段可执行的代码(通常是一个lambda表达式),这段代码在执行时应该抛出预期的异常。
以下是使用assertThrows改进后的正确测试代码:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows; // 导入JUnit 5的assertThrows
import java.io.ByteArrayInputStream;
import java.io.InputStream;
class ApplicationTest {
// 模拟Console.readLine()所需的输入流重定向
private void setSystemIn(String input) {
InputStream in = new ByteArrayInputStream(input.getBytes());
System.setIn(in);
}
@Test
void validateTheEnteredAmount_correctly() {
final String INVALID_NUMBER = "1234";
setSystemIn(INVALID_NUMBER); // 设置模拟输入
// 正确的断言方式:使用assertThrows验证异常
assertThrows(IllegalArgumentException.class, () -> Application.enterTheAmount());
}
}在这个修正后的测试中:
- assertThrows(IllegalArgumentException.class, ...)明确表示我们期望IllegalArgumentException被抛出。
- 第二个参数是一个lambda表达式() -> Application.enterTheAmount(),它封装了可能抛出异常的代码。
- 当Application.enterTheAmount()执行并抛出IllegalArgumentException时,assertThrows会捕获这个异常,并验证其类型是否与预期相符。如果相符,测试通过;如果不符或没有抛出异常,测试失败。
3. assertThrows 方法深度解析
assertThrows是JUnit 5提供的一个强大工具,用于验证方法是否抛出了特定的异常。
-
方法签名: assertThrows(Class
expectedType, Executable executable) - expectedType: 期望被抛出的异常的Class对象。
- executable: 一个Executable接口的实例,通常以lambda表达式的形式提供,包含可能抛出异常的代码。
- 返回结果: assertThrows方法会返回被捕获到的异常对象。这意味着我们可以进一步对这个异常对象进行断言,例如检查异常消息。
示例:进一步断言异常信息
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.assertj.core.api.Assertions.assertThat; // 结合AssertJ进行更丰富的断言
import java.io.ByteArrayInputStream;
import java.io.InputStream;
class ApplicationTest {
private void setSystemIn(String input) {
InputStream in = new ByteArrayInputStream(input.getBytes());
System.setIn(in);
}
@Test
void validateTheEnteredAmount_withExceptionMessageCheck() {
final String INVALID_NUMBER = "1234";
setSystemIn(INVALID_NUMBER);
// 捕获异常对象并进行进一步断言
IllegalArgumentException thrown = assertThrows(
IllegalArgumentException.class,
() -> Application.enterTheAmount(),
"当输入无效金额时,应该抛出IllegalArgumentException" // 失败时的可选消息
);
// 使用AssertJ断言异常消息
assertThat(thrown.getMessage())
.isNotNull()
.contains("金额必须是")
.contains("1000的倍数");
}
}在这个例子中,我们不仅验证了异常类型,还通过返回的thrown对象,使用AssertJ的assertThat对异常消息进行了更详细的断言,确保异常信息符合预期,这大大增加了测试的健壮性。
4. 注意事项与最佳实践
- 使用Lambda表达式: assertThrows的第二个参数必须是一个Executable或ThrowingCallable(AssertJ中),这意味着你需要将可能抛出异常的代码封装在一个lambda表达式或方法引用中。直接调用方法会导致异常在assertThrows执行之前就被抛出。
- 具体的异常类型: 尽可能断言最具体的异常类型。例如,如果方法抛出IllegalArgumentException,就断言IllegalArgumentException.class,而不是更泛化的RuntimeException.class或Exception.class。
- 测试范围: 确保你的测试只验证了预期的异常情况,而不是由于其他原因导致的意外异常。在示例中,我们通过setSystemIn精确控制了输入,以隔离测试条件。
- 与AssertJ的集成: 尽管assertThrows是JUnit 5的一部分,但它与AssertJ可以很好地结合使用。assertThrows用于捕获和验证异常类型,而AssertJ的assertThat可以用于对捕获到的异常对象(例如异常消息、原因等)进行更丰富的链式断言。
总结
在单元测试中验证预期异常是确保代码行为正确性的重要一环。避免直接对可能抛出异常的方法返回值进行assertThat().isInstanceOf()断言的常见误区。相反,应采用JUnit 5提供的assertThrows方法,它能够清晰、准确地捕获并验证特定类型的异常。通过将可能抛出异常的代码封装在lambda表达式中,并利用assertThrows返回的异常对象进行进一步的AssertJ断言,我们可以编写出既健壮又富有表达力的异常测试用例。










