单元测试和编码设计

时间:2012-04-02 11:27:54

标签: c# .net unit-testing design-patterns

在很多TDD教程中,我看到如下代码:

public class MyClass
{
    public void DoSomething(string Data)
    {
      if (String.IsNullOrWhiteSpace(Data))
        throw new NullReferenceException();
    }
}


[Test]
public DoSomething_NullParameter_ThrowsException()
{
   var logic = MyClass();

   Assert.Throws<NullReferenceException>(() => logic.DoSomething(null));
}

这一切都有意义,但在某些时候你会到达实际使用MyClass的类,并且你想测试是否处理了异常:

public class EntryPointClass
{
  public void DoIt(string Data)
  {
     var logicClass = new MyClass();

     try
     {
        logicClass.DoSomething(Data);
     }
     catch(NullReferenceException ex)
     {

     }
  }
}

[Test]
public DoIt_NullParameter_IsHandled()
{
  var logic = new EntryPointClass()

  try
  {
    logic.DoIt(null);
  }
  catch
  {
    Assert.Fail();
  }
}

那么为什么不把my / class中的try / catch放在首先,而不是抛出异常,并在MyClass单元测试类中测试为null,而不是在EntryPointClass单元测试类中?

5 个答案:

答案 0 :(得分:3)

通常,您的异常处理将如下所示:

public class EntryPointClass
{
  Logger _logfile;
  // ...
  public void DoIt(string Data)
  {
     var logicClass = new MyClass();

     try
     {
        logicClass.DoSomething(Data);
     }
     catch(NullReferenceException ex)
     {
         _logfile.WriteLine("null reference exception occured in method 'DoIt'");
     }
  }
}

Logger只是您在MyClass所在地不可用的正确异常处理所需资源的示例。您还可以在此处添加消息框的显示或类似内容这一点。)

MyClass级别,您通常没有适当的工具可用于正确的异常处理(并且您不希望在那里添加它们以保持类与特定的分离,例如,特定的记录机制)。

请注意,此设计决策与执行TDD无关。如果您希望您的班级MyClass实际上自己捕捉异常,您必须以不同的方式编写测试,这是正确的。但是,如果捕获异常并不会阻止您的自动测试,那么这只是一个好主意。例如,当MyClass单独显示硬编码警告对话框时,请尝试为MyClass编写一个良好的单元测试。

当然,上面的示例表明,当您第一次需要像记录器这样的东西来实现工作时,单元测试EntryPointClass可能会变得更难。通常,您可以通过使用ILogger接口在构造时提供记录器来解决此问题,该接口允许使用mock替换记录器。或者在这个简单的情况下,根本不要为单元测试初始化​​记录器并将其编码为:

 public class EntryPointClass 
 {
      // ....
      catch(NullReferenceException ex)
      {
          if(_logfile!=null)
              _logfile.WriteLine("null reference exception occured in method 'DoIt'");
      }    
      // ....
 }

答案 1 :(得分:1)

例外情况表明代码的异常情况(请参阅Exception Handling上的MSDN指南)。如果DoSomething的参数null是例外,那么无论 将如何使用该类,抛出它都是完全合理的。值得强调的是Exception Throwing部分正在详细描述它:

  

通过抛出异常来报告执行失败。如果某个成员无法成功完成它的设计,那么应该将其视为执行失败并抛出异常

更不用说,在撰写DoSomething时,你可能不知道会使用什么代码(实际上你不应该知道,也不关心)。

因此,如果异常抛出是代码合同的一部分 - 它应该进行测试,并且应该作为DoSomething测试的一部分进行测试。

答案 2 :(得分:1)

  

为什么不把try / catch放在MyClass中呢......

例外是用于解耦错误检测和错误处理。如果无法在MyClass中本地处理错误(在所有情况下),那么您应该抛出异常。

这允许本地错误检测(MyClass)和任何捕获异常的人的错误处理。


EntryPointClass测试中的测试,如果方法DoIt是空安全的。它与MyClass的使用没什么关系。

换句话说:EntryPointClass中的测试没有(和代码稳定性和封装问题)不应该依赖于使用MyClass的事实。

答案 3 :(得分:1)

单元测试测试工作单元

编写一个测试异常的测试然后将代码写入该规范并观察测试通过(这是您的第一个示例)

然后编写处理异常的代码(如果可能的话),如果这样做,则测试不应该考虑该异常,因为您已经处理了它,并且您应该测量该方法预期的输出。

如果您无法处理异常,则应省略catch步骤(除非您在此阶段进行日志记录)并在此情况下让它上升到堆栈,您再次断言该方法需要异常。

你永远不会在你的测试代码中加上try catch,你只断言是否抛出异常。

当您测试班级的正确功能时。

答案 4 :(得分:1)

您实际上正在测试两种非常不同的条件:

  1. 测试MyClass在参数无效时抛出异常
  2. 测试作为MyClass用户的EntryPointClass如何处理MyClass抛出的异常