10

我正在查看一个相当现代的项目,该项目非常强调单元测试。根据古老的格言“面向对象编程中的每个问题都可以通过引入新的间接层来解决”,这个项目正在运动多层间接。副作用是相当多的代码如下所示:

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

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

  1. 如果 balanceProvider.IsOverdraft() 返回 true 那么 IsOverdraft 应该返回 true
  2. 如果 balanceProvider.IsOverdraft() 返回 false 那么 IsOverdraft 应该返回 false
  3. 如果 balanceProvider 抛出异常,则 IsOverdraft 应该重新抛出相同的异常

更糟糕的是,使用的模拟框架 (NMock2) 接受方法名称作为字符串文字,如下所示:

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

这显然使“红色,绿色,重构”规则变为“红色,绿色,重构,在测试中重命名,在测试中重命名,在测试中重命名”。使用像 Moq 这样的不同模拟框架将有助于重构,但它需要对所有现有的单元测试进行扫描。

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

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

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

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

D)收拾一下,继续写那些测试;)

4

5 回答 5

5

B 或 C。这就是这种一般要求的问题(“每个方法都必须有单元测试,每一行代码都需要被覆盖”) - 有时,它们提供的好处不值得付出代价。如果这是您想出的,我建议重新考虑这种方法。“我们必须有 95% 的代码覆盖率”可能在纸面上很有吸引力,但实际上它很快就会产生像你所遇到的问题。

此外,您正在测试的代码是我称之为trivial code的代码。对其进行 3 次测试很可能是矫枉过正。对于那一行代码,您将不得不维护 40 多个。除非您的软件是关键任务(这可能解释了高覆盖率要求),否则我会跳过这些测试。

Kent Beck 不久前在这个网站上提供了关于这个主题的(恕我直言)最实用的建议之一,我在我的博客文章中对这些想法进行了一些扩展——你应该测试什么?

于 2013-01-20T00:06:39.500 回答
4

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

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

在您的示例中,通过直接阅读实现比测试更容易理解发生了什么。该测试在文档方面没有增加任何价值。所以我很乐意删除它。

最重要的是,此类测试非常脆弱,因为它们与实现紧密耦合。从长远来看,当您需要重构东西时,这是一场噩梦,因为任何时候您想要更改它们都会破坏的实现。

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

于 2013-01-20T00:44:55.770 回答
1

我认为单元测试要记住的最重要的事情之一是,今天如何实现代码并不一定重要,而是当测试的代码(直接或间接)在未来被修改时会发生什么。

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

因此,如果这是我的应用程序,我会首先将只进调用减少到最低限度(降低代码复杂性),然后引入一个不依赖字符串值作为方法名称的模拟框架。

于 2013-01-20T00:20:13.523 回答
1

有几件事要添加到这里的讨论中。

立即逐步切换到更好的模拟框架。 大约 3 年前,我们从 RhinoMock 切换到 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 使这变得非常简单和安全。

此外,对于有意义的委派或委派场景,您无法摆脱但需要测试,这样的事情会使其变得容易得多。

于 2013-01-21T03:26:46.340 回答
0

我会说 D) 把它收起来,继续编写这些测试;) 并尝试看看你是否可以用 MOQ 替换 NMock。

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

于 2013-01-19T23:59:40.673 回答