单元测试中的多个断言

时间:2010-05-20 23:59:22

标签: unit-testing mstest assert

我刚刚阅读了Roy Osherove的“单元测试艺术”,我试图坚持他在书中列出的最佳实践。其中一个最佳实践是不在测试方法中使用多个断言。这个规则的原因对我来说相当清楚,但这让我很奇怪......

如果我有类似的方法:

public Foo MakeFoo(int x, int y, int z)
{
     Foo f = new Foo();
     f.X = x;
     f.Y = y;
     f.Z = z;

     return f;
}

我是否必须真正编写单独的单元测试以断言Foo的每个单独属性是否使用提供的值进行初始化?在测试方法中使用多个断言真的很少见吗?

仅供参考:我正在使用MSTest。

编辑:感谢所有回复。我想我最终将继续使用多个断言。在我的情况下,正在测试的行为是MakeFoo制作一个合适的Foo。因此断言每个属性都达到预期值就足够了。但是,如果设置其中一个属性存在条件,那么我将分别测试每个单独的结果。

我仍然不喜欢它....我喜欢每次测试一个断言的想法是你知道测试失败的确切原因。如果您解决了问题,那么测试将通过。有多个断言,您没有相同的保证。如果你修复了失败的断言所提到的问题,那么在测试中的下一次失败就没有什么可以阻止另一个断言了。如果断言被分开,那么你从一开始就知道这两个失败。

最后,我不仅仅使用.Equals()的原因是因为在我的情况下,Foo是一个LINQ-To-SQL实体,它引入了一些不值得进入的复杂功能。

再次感谢。

7 个答案:

答案 0 :(得分:16)

每单元测试只测试一个概念更为重要。

测试一个概念可能需要不止一个断言,所以不要过分担心断言的数量。当然,如果您最终得到大量断言,您可能需要退一步思考一下您的测试内容。

答案 1 :(得分:5)

在这样的情况下,如果我试图对每个测试的一个断言严格,我将在Foo上声明相等,而不是它的组件。这促使我写了Foo.equals(),这通常是一件好事。但是我并不总是严格要求每次测试一个断言。看看对你有什么好处。

答案 2 :(得分:4)

AFAI看,这种做法背后的原理是

  • 每次测试测试一个内聚/逻辑的东西;导致简洁易读的测试。
  • 有更好的测试隔离。一个测试不应该由于n个原因之一而失败。如果确实如此,那么如何解决它并不是很明显;没有先调查哪个原因导致它失败。一次看似无关的测试不应该导致一次改变。
  • 来自TDD初学者规则手册的入门规则;这样他们就可以将过程内化。

关键不在于每个测试需要一个'NUNit'/'xUnit'断言;相反,每次测试都有一个逻辑断言。

[Test]
public void MakeFoo_SeedsInstanceWithSpecifiedAttributes()
{
  Assert.AreEqual( new Foo(1,2,3), Foo.MakeFoo(1,2,3), "Objects should have been equal );
}

这里我有一个NUnit断言(保持断言警察高兴):然而它正在测试幕后的三件事(在Foo.Equals中我会检查所有变量是否相等。)
该指南旨在防止类似的测试(即伪装成单元测试的集成测试)

[Test]
public void ITestEverything()
{
  // humongous setup - 15 lines
  payroll.ReleaseSalaries();
  // assert that the finance department has received a notification
  // assert employee received an email
  // assert ledgers have been updated
  // statements have been queued to the printqueue
  // etc.. etc..
}

故事的道德:这是一个很好的目标。尝试将所有想要测试的东西组合成一个具有良好名称的逻辑/有凝聚力的Assert。例如Assert.True(AreFooObjectsEqual(f1,f2)。如果您发现在命名内聚的验证/断言时遇到困难 - 也许您需要检查测试并查看是否需要拆分它。

答案 3 :(得分:2)

我实际上已经编写了一个NUnit插件来帮助解决这个问题。在http://www.rauchy.net/oapt

处试试

答案 4 :(得分:1)

在测试中使用多个断言很常见,但我不认为这是“最佳做法”。

我会说是的,你确实想对上面的函数使用多个测试,所以你可以看到问题出在哪里。我最近一直在使用一个相当大的测试套件,它有一些奇怪的故障,我还没有跟踪,而我遇到的问题是每个测试用例都做得太多了。我把它们分开了,现在我可以禁用那些失败的特定的那些,这样我就可以在有机会的时候回到它们身上。

但是,您仍然可以使用夹具来分解设置和拆卸的通用性。我无法与MSTest交谈,但在UnitTest ++中你会做类似的事情:

class FooFixture
{
public:
   FooFixture() : f(MakeFoo(kX, kY, kZ)) { }

   static const int kX = 1;
   static const int kY = 2;
   static const int kZ = 3;

   Foo f;
};

TEST_FIXTURE(FooFixture, IsXInitializedCorrectly)
{
   CHECK_EQUAL(kX, f.X);
}

// repeat for Y and Z

这不是更多的打字,尤其是剪切和粘贴。哎呀,在vim中它只是esc y ctrl-{ pp然后进行了轻微的编辑。但是,通过这种设置,您可以看到它是否只是一个字段失败然后深入了解原因。

答案 5 :(得分:1)

我通常发现自己在一个方法中有多个断言,但它们通常都与手头的测试有关。我有时会在断言中查明我知道会破坏测试的前提条件,因为我不希望测试成功。

[Test] // NUnit style test.
public void SearchTypeAndInventory() 
{
    var specification = new WidgetSearchSpecification 
                    {
                        WidgetType = Widget.Screw,
                        MinimumInventory = 10
                    }; 
var widgets = WidgetRepository.GetWidgets(specification);
if( !widgets.Any() ) 
    Assert.Inconclusive("No widgets were returned.");
Assert.IsTrue( 
    widgets.All( o => o.WidgetType == Widget.Screw), 
    "Not all returned widgets had correct type");
Assert.IsTrue( 
    widgets.All( o => o.InventoryCount >= 10), 
    "Not all returned widgets had correct inventory count.");

*虽然我可以合并Asserts,但我发现将错误分开会更有用。


我认为坚持1-assert-per-test的硬规则非常有用。更重要的是,测试只测试一件事。我已经看过许多超级测试,这是一种测试课程所有内容的巨大方法。这些超级测试很脆弱,难以维护。

您应该认为您的测试与他们测试的代码一样重要且编写得很好。例如。应用相同的重构技术,以确保测试做一件事并做得好,并不是一个测试所有东西的瘦腿。

答案 6 :(得分:0)

我读书的方式,你应该做“红色,绿色,重构”。在“重构”部分中,您应该重构被测代码和单元测试。

我认为以下重构没有错:

原始

[TestMethod]
public void TestOne()
{
    LetsDoSomeSetup();
    AssertSomething();
}

[TestMethod]
public void TestTwo()
{
    LetsDoSomeSetup(); // Same setup
    AssertSomethingElse();
}

重构

[TestMethod]
public void TestOneTwo()
{
    LetsDoSomeSetup();
    AssertSomething();
    AssertSomethingElse();
}

当然,这假设两个断言是相关的,当然也依赖于相同的场景。