如何在具有多个间接层的项目中使用单元测试

时间:2013-01-19 23:41:16

标签: c# unit-testing mocking tdd

我正在寻找一个非常现代化的项目,重点是单元测试。按照古老的格言“面向对象编程中的每个问题都可以通过引入新的间接层来解决”这个项目是多层间接的。副作用是相当数量的代码如下所示:

public bool IsOverdraft)
{
    balanceProvider.IsOverdraft();
}

现在,由于单元测试的重点和维护高代码覆盖率,每一段代码都有针对它编写的单元测试。因此,这个小方法将有三个单元测试。那些会检查:

  1. 如果balanceProvider.IsOverdraft()返回true,则IsOverdraft应返回true
  2. 如果balanceProvider.IsOverdraft()返回false,则IsOverdraft应返回false
  3. 如果balanceProvider抛出异常,则IsOverdraft应重新抛出相同的异常
  4. 更糟糕的是,使用的模拟框架(NMock2)接受了方法名称作为字符串文字,如下所示:

    NMock2.Expect.Once.On(mockBalanceProvider)
        .Method("IsOverdraft")
        .Will(NMock2.Return.Value(false));
    

    这显然使“红色,绿色,重构”规则变为“红色,绿色,重构,重新测试,重新测试,重新测试”。使用不同的模拟框架(如Moq)可以帮助重构,但需要扫描所有现有的单元测试。

    处理这种情况的理想方法是什么?

    A)保持较小级别的层次,以便不再发生这些转发呼叫。

    B)不要测试那些转发方法,因为它们不包含业务逻辑。出于覆盖的目的,使用ExcludeFromCodeCoverage属性标记它们。

    C)仅在调用适当的方法时进行测试,而不检查返回值,异常等。

    D)吸收它,并继续编写这些测试;)

5 个答案:

答案 0 :(得分:5)

B或C.这是一般要求的问题(“每个方法都必须进行单元测试,每行代码都需要覆盖”) - 有时,他们提供的好处是不值得的成本。如果这是你想出的东西,我建议重新考虑这种方法。 “我们必须拥有95%的代码覆盖率”可能在纸面上具有吸引力,但在实践中它很快就会产生类似你所拥有的问题。

此外,您正在测试的代码是我称之为普通代码的代码。对它进行3次测试很可能是矫枉过正。对于那一行代码,你必须保持40多个。除非您的软件是关键任务(这可能解释了高覆盖率要求),否则我会跳过这些测试。

其中一个(恕我直言)most pragmatic advices on this topic是肯特贝克前一段时间在这个网站上提供的,我在博客文章中对这些想法进行了一些扩展 - What should you test?

答案 1 :(得分:4)

老实说,我认为我们应该只编写测试来以有用的方式记录我们的代码。我们不应仅仅为了代码覆盖而编写测试。 (代码覆盖率只是一个很好的工具,可以找出它未被覆盖的内容,以便我们可以弄清楚我们是否忘记了重要的单元测试用例,或者我们是否真的在某处有一些死代码。)

如果我写了一个测试,但是测试最终只是实现的“重复”或者更糟......如果,那么理解测试比实际实现更难< /strong>那真的这样的测试不应该存在。没有人有兴趣阅读这样的测试。 测试不应包含实施细节。测试是关于“what”应该发生的“how”它将会完成。既然你用“TDD”标记了你的问题,我想补充说TDD是一种设计实践。因此,如果我已经提前100%确定将要实现的设计是什么,那么我就没有必要使用TDD和编写单元测试(但是我会在所有情况下都有高级验收测试,涵盖该代码)。当设计的东西非常简单时(例如在你的例子中),这种情况经常发生。 TDD不是关于测试和代码覆盖,而是关于帮助我们设计代码和记录代码。没有必要使用设计工具或文档工具来设计/记录简单/明显的事物。

在您的示例中,通过直接读取实现而不是测试来了解正在发生的事情要容易得多。测试不会在文档方面增加任何价值。所以我很乐意把它抹掉。

除此之外此类测试非常脆弱,因为它们紧密耦合到实现。从长远来看,这是一场噩梦,因为任何时候你都想要改变它们将会破坏的实现。

我建议做的是,不要编写这样的测试,而是进行更高级别的组件测试或快速集成测试/验收测试,这些测试将在不了解内部工作的情况下运用这些层。

答案 2 :(得分:1)

我认为单元测试要记住的最重要的事情之一是,它并不一定与今天的代码实现方式有关,而是当直接或间接测试的代码在将来

如果您今天忽略这些方法并且它们对您的应用程序的操作至关重要,那么有人决定在未来的某个时刻实施新的balanceProvider或者决定重定向不再有意义,您很可能会遇到故障点

所以,如果这是我的应用程序,我首先会考虑将仅向前调用减少到最低限度(降低代码复杂性),然后引入一个不依赖于方法名称的字符串值的模拟框架。 / p>

答案 3 :(得分:1)

这里要讨论的几件事情。

立即逐步切换到更好的模拟框架。大约3年前,我们从RhinoMock切换到Moq。所有新测试都使用Moq,通常当我们更改测试类时,我们会将其切换。但是代码中没有太大变化或者有大量测试成本的区域仍在使用RhinoMock,这没关系。由于进行切换,我们日常工作的代码要好得多。所有测试更改都可以以增量方式进行。

您正在编写太多测试。在TDD中要记住的一件重要事情是,您应该只编写代码以满足红色测试,并且您应该只编写一个测试来指定一些不成文的测试码。因此,在您的示例中,三个测试是过度的,因为最多需要两个测试来强制您编写所有生产代码。 异常测试不会让你编写任何新代码,所以不需要编写它。我可能只会编写这个测试:

[Test]
public void IsOverdraftDelegatesToBalanceProvider()
{
    var result = RandomBool();
    providerMock.Setup(p=>p.IsOverdraft()).Returns(result);
    Assert.That(myObject.IsOverDraft(), Is.EqualTo(result);
}

不要创建无用的间接层。大多数情况下,单元测试会告诉您是否需要间接。大多数间接需求可以通过依赖性倒置原则来解决,或者“耦合到抽象,而不是结果”。由于其他原因需要一些层(我使WCF ServiceContract实现成为薄层传递层。我也不测试该传递)。如果你看到一个无用的间接层,1)确保它真的没用,那么2)删除它。随着时间的推移,代码混乱会带来巨大的成本。 Resharper使这个过于简单和安全。

此外,对于有意义的委派或委派方案,您无法摆脱但需要进行测试,something like this使其变得更加容易。

答案 4 :(得分:0)

我会说D)把它吸干,并继续编写这些测试;)并试着看看你是否可以用MOQ取代NMock。

它可能似乎没有必要,即使它现在只是委托,但测试正在测试它正在使用正确的参数调用正确的方法,并且方法本身在返回值之前没有做任何时髦的事情。所以在测试中覆盖它们是个好主意。但是为了更容易使用MOQ或类似的框架,这将使它更容易重构。