测试是否抛出正确的异常(Test throwing the right exception)

[b]问题:[/b]
你是否想过异常也要去测试?你想了怎么验证一个方法是否在某种特定的情况下抛出期望的异常,也许你正在找是否要测试,以及有没有简单的测试方法

[b]背景:[/b]
要想知道任何实现这种测试,你需要了解JUnit如何判定一个测试时通过还是失败。如果一个断言失败或者抛出一个异常的时候,测试就会失败,否则测试就通过。换句话说,如果一个测试全部走完,就是说程序从头运行到了尾,而没有从中间跳过,那么它就通过了。知道了这些,就足够你推断出然后写这种测试了:如果该抛出异常的代码段没有抛出异常,那么这个测试就应该失败;测试只能捕捉期望的异常;任何其他的异常都应该由JUnit框架捕捉。

[b]诀窍:[/b]
下面的代码展示了,如何写这种验证是否抛出正确异常的测试示例:

public void testConstructorDiesWithNull() throws Exception{
try{
Fraction oneOverZero = new Fraction(1,0);
fail("Created fraction 1/0! That's undefined!");
}
catch(IllegalArgumentException excepted){
assertEquals("denominator",excepted.getMessage());
}
}

首先分析上面的代码
1. 找到可能抛出异常的代码段,将它放入一个try语句内。
2. 调用了应该抛出异常的方法以后,在try语句内写一个fail()方法来说明:“如果运行到了这里,那么说明期望的异常没有被抛出”。
3. 添加一个catch语句以捕获期望的异常。
4. 在catch语句内,如果需要的话,验证捕获的异常的属性与你期望的相同。
5. 声明该测试方法会抛出异常,这可以让代码适应性更强。有人可能会在测试程序外声明这个方法可抛出其他的异常,这种变化不应该影响你的测试,因此它不应该导致你的测试不能被编译。

[b]讨论:[/b]
如果测试的方法抛出其他的异常---------不同于你要捕获的异常-----那么JUnit将报告一个error,而不是failure,因为测试方法将一个异常抛出给JUnit框架。记住这样的error一般是环境或者测试程序自身的错误,而不是产品代码的问题。如果产品代码抛出了一个预期之外的异常,那么一般可能是潜在的问题阻碍了测试的正常运行。

[b]一个更面向对象的解决办法[/b]
先看一个匿名内部类

public void testForException(){
assertThrows(MyException.class, new ExceptionClosure(){
public Object execute(Object input) throws Exception{
return doSomethingThatShouldThrowMyException();
}
});
}

虽然有人觉得Java的匿名内部类不太好读,但这个方法的意图再明显不过了:"测试这段代码是否抛出期望的异常"。可以按如下的方式实现assertThrows()方法:

public static void assertThrows(Class expectedExceptionClass, ExceptionalClosure closure){
String expectedExceptionClassName = expectedExceptionClass.getName();

try{
closure.execute(null);
fail("Block did not throw an exception of type"+expectedExceptionClassName);
}
catch(Exception e){
assertTrue("Caught exception of type <" + e.getClass().getName()
+">, expected one of type <"
+expectedExceptionClassName+">",
expectedExceptionClass.isInstance(e));
}
}

我们捕捉了所有的异常,而不仅是期望的异常,这是因为当编译的时候,我们还不知道代码会抛出什么样的异常。如果这样设置断言,那么错误信息就很重要,因为你取走了错误信息的控制权。另外一个可选的方案是,在assertThrows()方法中添加另外一个参数,用来接收自定义的异常,最后,因为我们必须测试所有的异常,我们必须将捕获的异常是否是期望的异常的实例,这与使用instanceof()方法是一样的。

[b]注意写的断言:[/b]
如果你的断言与特定的异常相关,那么你要小心:如果验证异常对象的结构过于紧凑,那么可能导致测试与产品代码耦合得过强。这时候,测试的结果可能有点不太可靠。假设异常信息是给终端用户看,而你要写直接验证该消息的断言。一般来说,你会照着如下的方式编写测试代码:

public void testNameNoEntered(){
try{
log("", "password");
fail("User logged in without a user name !");
}
catch(MissingEntryException expected){
assertEquals("userName", expected.getEnteryName());
assertEquals("Please enter a user name and try again",
expected.getMessage());

}
}

测试代码看上去很清楚:如果一个用户不输入用户名的情况下登录,那么登录模块就抛出一个MissingEntryException。这个异常包含了登录所缺少的必要项的名称,以及提供给终端用户的信息。这看起来很好,因为这个异常对象包含的信息非常简单,它包含的数据像终端用户读取的信息一样易于理解。虽然catch语句中的第一断言写得不错,但是第二个有点不同的意见,虽然userName是程序的内部名称,并且是行为的一部分,但给终端用户看的信息可以在不影响登录功能的前提下随时改变。换句话说,如果userName改变的话,测试程序就需要随之改变。然而,测试似乎不应该随着终端用户信息的改变而改变,由于测试时现在写的,将来任何属性值的改变,都会要求测试程序随之改变。

这个例子中,我们建议去除第二个断言而仅保留第一个,因为涉及的一般准则是:将给终端用户看的信息与内部对象的行为相分离。你可以经常简单地初始化应该异常对象,然后检查它的toString()以及getMessage()方法的返回值。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值