【问题标题】:Testing Exceptions with Nunit使用 Nunit 测试异常
【发布时间】:2014-08-14 13:33:05
【问题描述】:

我正在使用 Nunit 进行单元测试,我需要对抛出异常的代码进行单元测试。我的代码与此类似。

    public class Myclass
    {
        public int Count
        {
            get; set;
        }
        public void Foo()
        {
            try
            {
                if (Count >3)
                {
                    throw new Exception();
                }
            }
            catch (Exception e)
            {
                MessageBox.Show(e.Message);
            }
        }
    }

   [TestFixture]
  public class TestMyClass
  {
    [Test]
    public void TestFoo()
    {
        var obj = new Myclass();
        obj.Count = 4;
        Assert.Throws<Exception>(obj.Foo);
    }
 }

这会产生这样的错误

  Expected: <System.Exception>
  But was:  null

我发现如果我删除 try catch 块,单元测试通过。但我不想更改实际代码。请告知我如何对上述代码进行单元测试以及正确的做法。

【问题讨论】:

  • 对于初学者来说,您不应该在要进行单元测试的代码中包含消息框(或任何其他 gui 代码)。另请注意,您的代码有效地吞下了异常。它将它显示为一个消息框,但它不会重新抛出它,因此您的单元测试将永远不会看到它。简而言之,您需要更改实际代码,因为它不是为了测试而编写的。而no,这里的正确方法是不去弄清楚如何测试是否显示了消息框。这里正确的做法是重构代码,将GUI代码与其背后的逻辑分开。
  • @LasseV.Karlsen 感谢您的建议。我也认为这可能是原因,但是如果我注释掉消息框行并且只有一个空的 catch 块,它仍然会失败并给出相同的错误。我愿意不明白其中的原因。
  • 空的 catch 块捕获异常但不重新抛出它。你基本上是在说“我会接受这个例外并忽略它”。简而言之,不要那样做。在 catch 块中添加 throw; 行,您的单元测试应该会看到异常,但如果这就是 try/catch 所做的一切,您应该将其删除。同样,您不应该在通过单元测试执行的代码中包含 GUI 代码。
  • 如果您需要将 GUI 响应输出到其他代码,您应该找到一种不同的方法来完成。通过返回保证消息框的结果,但该消息框由调用您的函数的 GUI 代码处理,或者将对象注入可用于引发事件的对象/方法中,您的 GUI 代码将显示一个消息框来响应。
  • 请注意,错误处理不是一个可以掉以轻心的主题。如果在向最终用户显示异常后重新抛出异常,它可能最终会被 .NET 运行时捕获,该运行时会将其作为未捕获的异常处理,并可能终止您的程序。如果您希望您的逻辑以可以测试它是否被抛出的方式抛出异常,则不应使用其中的消息框,也不要捕获它(至少不要吞下它)。然后,在 GUI 代码中,您将对方法的调用包装在 try/catch 中。

标签: c# unit-testing exception nunit


【解决方案1】:

Lee's answer 是正确的,但我想扩展您通过实施它获得的收益。

您的设计很好,但不适合测试。问题是双重的:

  1. 您将类依赖项隐藏在静态方法调用后面 (MessageBox.Show)
  2. 您依赖具体的实现 (MessageBox) 而不是抽象

修复第二个问题将使代码可测试。您必须引入您将在单元测试中验证的假对象(如 Lee 建议的那样)。 正确修复第二个问题(即通过接口注入依赖项)也将解决第一个问题。

通过引入接口,我们做了两件事:

  1. 消息框对一般错误处理组件的抽象使用——我们不再依赖纯gui相关的MessageBox。实际的错误处理程序仍然可能是一个消息框,但现在它也可以是自定义窗口、系统警报或日志消息。你的班级不需要知道它是什么,现在它不需要(我们已经分离了关注点)。
  2. 将所述错误处理程序的依赖项引入到测试类 - 通过这一更改,我们已经大声明确地声明,“要正常运行,该类需要错误处理程序”。对于以后使用您的代码的任何人来说,这都是重要的信息。

【讨论】:

  • 感谢 Jimmy,通过这次讨论,我意识到如果我已经知道并处理情况,那么它们不是异常,它们是逻辑情况,应该是我的主要代码的一部分,只有意外情况应该进入 catch 块采取任何预防措施。您认为其理解正确吗?
  • @TRS:是的,你是对的。在同一方法中抛出和捕获异常通常是多余的(您可以在满足条件时简单地处理错误,Count &gt; 3,跳过异常部分)。然而,无论错误是如何产生的,提取错误处理逻辑(如 Lee 所建议的)都是可行的方法。
【解决方案2】:

由于您的方法会吞下所有异常,因此您不能使用Assert.Throws,因为不会引发异常。

如果您想检查是否以某种方式处理了异常,您可以创建一个接口:

public interface IExceptionHandler
{
    void Handle(Exception ex);
}

public void WinFormsExceptionHandler : IExceptionHandler
{
    public void Handle(Exception ex)
    {
        MessageBox.Show(e.Message);
    }
}

public class Myclass
{
    private readonly IExceptionHandler handler;

    public Myclass(IExceptionHandler handler) { this.handler = handler; }
    public Myclass() : this(new WinFormsExceptionHandler()) { }
    public int Count
    {
        get; set;
    }
    public void Foo()
    {
        try
        {
            if (Count >3)
            {
                throw new Exception();
            }
        }
        catch (Exception e)
        {
            this.handler.Handle(e);
        }
    }
}

然后您可以在测试中使用模拟,例如使用 RhinoMocks 您可以这样做:

[Test]
public void TestFoo()
{
    var handler = MockRepository.GenerateMock<IExceptionHandler>();
    var obj = new Myclass(handler);
    obj.Count = 4;
    obj.Foo();

    handler.AssertWasCalled(h => h.Handle(Arg<Exception>.Is.Anything));
}

【讨论】:

  • 谢谢 Lee,这正是我一直在寻找的。我确实有一个 FailureManager,我在我的 catch 块中使用它,但它现在是静态的。我想我可以改变它,我也在使用 rhino 模拟所以您的解决方案非常合适。
猜你喜欢
  • 2011-07-31
  • 2013-03-16
  • 2011-12-18
  • 2013-10-08
  • 2016-05-05
  • 2014-01-04
  • 1970-01-01
  • 2016-01-21
  • 2019-11-11
相关资源
最近更新 更多