了解一些单元测试实践

时间:2012-06-04 23:14:45

标签: c# unit-testing nunit

我是单元测试的新手 - 我只使用纯粹的Testmethods进行了基本的断言测试(我的最后一个模块,我创建了大约50个)。

我目前正在阅读一本关于单元测试的书,本书中的许多例子中的一个让我为每个单独的测试创建一个新类。下面是为一个测试用例创建的示例对象之一。我的问题是有必要这样做吗?或者什么时候应该采用这种方法,什么时候不必要?

  public class and_saving_an_invalid_item_type : when_working_with_the_item_type_repository
    {
        private Exception _result;

        protected override void Establish_context()
        {
            base.Establish_context();

            _session.Setup(s => s.Save(null)).Throws(new ArgumentNullException());
        }

        protected override void Because_of()
        {
            try
            {
                _itemTypeRepository.Save(null);
            }
            catch (Exception exception)
            {
                _result = exception;
            }
        }

        [Test]
        public void then_an_argument_null_exception_should_be_raised()
        {
            _result.ShouldBeInstanceOfType(typeof(ArgumentNullException));
        }
    }

2 个答案:

答案 0 :(得分:3)

您是否需要为每个单独的测试创建一个新课程?我会说不,你当然不会。我不知道为什么这本书会这么说,或者他们只是为了帮助说明他们的例子。

为了回答你的问题,我建议为每个测试使用一个类...但它实际上要比这复杂一点,因为你定义“组”的方式是多变的,取决于你当时在做什么。

根据我的经验,一组测试实际上是逻辑结构的,就像一个文档,它可以包含一组或多组测试,通过一些共同的方面组合(有时嵌套)。用于测试面向对象代码的自然分组是按类分组,然后按方法分组。

这是一个例子

  • 测试1级
    • 测试方法1
      • 方法1的主要行为
      • 方法1的替代行为
    • 测试方法2
      • 方法2的主要行为
      • 方法2的替代行为

不幸的是,在C#或java(或类似的语言)中,你只有两个级别的结构可以使用(而不是你真正想要的3或4),所以你必须要破解东西以适应。

这样做的常用方法是使用一个类将一组测试组合在一起,而不是在方法级别对任何内容进行分组,如下所示:

class TestsForClass1 {
  void Test_method1_primary()
  void Test_method1_alternate()

  void Test_method2_primary()
  void Test_method2_alternate()
}

如果您的方法1和方法2都具有相同的设置/拆卸,那么这很好,但有时他们没有,导致这种分解:

class TestsForClass1_method1 {
  void Test_primary()
  void Test_alternate()
}

class TestsForClass1_method2 {
  void Test_primary()
  void Test_alternate()
}

如果您有更复杂的要求(假设您对method_1有10个测试,前5个有设置要求X,接下来有5个有不同的设置要求),那么人们通常最终只会制作越来越多的类名:

class TestsForClass1_method1_withRequirementX {  ... }
class TestsForClass1_method1_withRequirementY {  ... }

这很糟糕,但是嘿 - 方钉,圆洞等等。

就个人而言,我喜欢在方法中使用lambda函数来为你提供第三级分组。 NSpec显示了一种方法可以做到这一点......我们有一个内部测试框架,略有不同,它有点像这样:

class TestsForClass1 {
   void TestsForMethod1() {
      It.Should("perform it's primary function", () => {
         // ....
      });

      It.Should("perform it's alternate function", () => {
         // ....
      });
   }
}

这有一些缺点(如果第一个It语句失败,其他语句不运行),但我认为这种权衡是值得的。)


- 最初的问题是:“是否真的有必要为我想要执行的每个测试创建一个对象?”。根据这个解释,答案是(大多数情况下)是肯定的。

通常,单元测试涉及两部分的交互

  • 被测物体。通常这是您编写的类或函数的实例
  • 环境。通常,这是您传递给函数的任何参数,以及该对象可能引用的其他依赖项。

为了使单元测试可靠,每个测试都需要“新鲜”,以确保系统状态良好且可靠。

  • 如果每个测试都没有刷新被测试的东西,那么一个函数可能会改变对象的内部状态,导致下一个测试错误地失败

  • 如果没有为每个测试刷新环境,那么一个函数可能会改变环境(例如:在外部数据库中设置一些变量或其他东西),这可能导致下一个测试错误地失败。

显然很多情况并非如此 - 例如,你可能有一个纯粹的数学函数,它只将整数作为参数而不会触及任何外部状态,然后你可能不想再费心了被测对象或测试环境......但一般来说,任何面向对象系统中的大多数东西都需要刷新,所以这就是为什么这是“标准做法”。

答案 1 :(得分:2)

我无法关注您的示例,但理想情况下,任何测试用例都应该能够独立于任何其他测试用例运行 - 独立于任何其他其他。