如何为异常编写单元测试?

36

你知道,异常是在异常情况下抛出的。那么如何模拟这些异常呢?我觉得这是一项挑战。对于这样的代码片段:

public String getServerName() {
    try {

        InetAddress addr = InetAddress.getLocalHost();
        String hostname = addr.getHostName();
        return hostname;
    }
    catch (Exception e) {
        e.printStackTrace();
        return "";
    }
}

有人有好的想法吗?


2
出于好奇,为什么这是一个 CW? - Uri
他可能已经注意到了大多数问题的第一个评论是“应该是维基”。这个问题可能有6个或更多有效答案,所以将其设置为社区维基(CW)似乎是合理的。 - Frank Schwieterman
5个回答

72

你可以让JUnit知道正确的行为是抛出异常。

在JUnit4中,大致用法如下:

@Test(expected = MyExceptionClass.class) 
public void functionUnderTest() {
    …
}

38

其他回答已经涉及了如何编写检查异常是否被抛出的单元测试的一般问题。但我认为你的问题实际上是关于如何在代码中首先引发异常。

以你的代码为例。在简单的单元测试上下文中,很难导致你的getServerName()内部引发异常。问题在于为了发生异常,代码(通常)需要在网络有问题的计算机上运行。在单元测试中安排这种情况可能是不可能的...你需要在运行测试之前故意配置错误的机器。

那么答案是什么呢?

  1. 在某些情况下,简单的解决方案就是做出实用的决定,不要追求完全的测试覆盖率。你的方法就是一个很好的例子。从代码检查中应该清楚方法实际上是做什么的。测试它并不能证明任何东西(除了参见下面的**)。你所做的只是提高你的测试计数和测试覆盖率数字,这两者都不应该是项目的目标。

  2. 在其他情况下,将生成异常的低级代码分离出来并将其变成独立的类可能是明智的。然后,为了测试高级代码对异常的处理,您可以使用模拟类替换该类,以引发所需的异常。

这是你的例子经过这种“处理”的结果。(这有点牵强...)

public interface ILocalDetails {
    InetAddress getLocalHost() throws UnknownHostException;
    ...
}

public class LocalDetails implements ILocalDetails {
    public InetAddress getLocalHost() throws UnknownHostException {
        return InetAddress.getLocalHost();
    }
}

public class SomeClass {
    private ILocalDetails local = new LocalDetails();  // or something ...
    ...
    public String getServerName() {
        try {
            InetAddress addr = local.getLocalHost();
            return addr.getHostName();
        }
        catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }
}
现在要进行单元测试,需要创建一个“模拟”实现ILocalDetails接口的对象,使其getLocalHost()方法在适当的条件下抛出所需的异常。然后,为SomeClass.getServerName()创建一个单元测试,让SomeClass的实例使用您的“模拟”类的实例而不是正常的类实例。(最后一步可以使用模拟框架通过暴露local属性的setter或使用反射API来完成)。
显然,您需要修改代码以使其可测试。还有一些限制...例如,现在您无法创建一个单元测试来使真正的LocalDetails.getLocalHost()方法抛出异常。您需要逐个判断是否值得这样做;即单元测试的好处是否超过了以这种方式使类可测试所需的工作量(和额外的代码复杂性)。(这种情况下静态方法的存在是问题的重要部分。)
**这种测试确实有一个假设的点。在您的示例中,原始代码捕获异常并返回空字符串可能是一个错误,具体取决于方法的API如何规定,而假设单元测试将发现它。但是,在这种情况下,这个错误非常明显,您在编写单元测试时就会发现它!并且假设您在发现错误时修复它,那么该单元测试将变得有些多余。(您不会期望有人恢复这个特定的错误...)

没错,这就是我想要的。Mock是一种实现这个目标的方法,但是需要调整生产代码并付出额外的努力。你认为这种Mock方法能够捕捉到一些bug吗? - Joseph
如果有错误需要捕获,是的。但这取决于上下文,即您正在测试的代码。 - Stephen C

15

这里有几个可能的答案。

测试异常本身很容易。

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

@Test
public void TestForException() {
    try {
        doSomething();
        fail();
    } catch (Exception e) {
        assertThat(e.getMessage(), is("Something bad happened"));
    }
}

或者,您可以使用异常注释来指出您期望出现异常。

现在,就您的具体示例而言,在您无法与对象交互的情况下测试您在方法中创建的某些内容(通过 new 或静态方式),是很棘手的。通常需要封装该特定生成器,然后使用一些模拟技术来覆盖生成您期望的异常行为。


9

由于这个问题在社区维基中,我将为完整性添加一个新问题: 您可以在JUnit 4中使用ExpectedException

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

@Test
public void TestForException(){
    thrown.expect(SomeException.class);
    DoSomething();
}
ExpectedException可以使所有测试方法都能够访问抛出的异常。
还可以测试特定的错误消息:
thrown.expectMessage("Error string");

或者使用匹配器。
thrown.expectMessage(startsWith("Specific start"));

这比原来更短更方便。

public void TestForException(){
    try{
        DoSomething();
        Fail();
    }catch(Exception e) {
      Assert.That(e.msg, Is("Bad thing happened"))
    }
}

因为如果您忘记了失败,测试可能会导致虚假负面结果。

7

许多单元测试框架允许您的测试作为测试的一部分期望异常。例如,JUnit允许这样做

@Test (expected=IndexOutOfBoundsException.class) public void elementAt() {
    int[] intArray = new int[10];

    int i = intArray[20]; // Should throw IndexOutOfBoundsException
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接