测试与不要重复自己(DRY)


11

为什么如此鼓励通过编写测试来重复自己?

看起来测试基本上表达了与代码相同的内容,因此是代码的重复(从概念上讲,不是实现)。DRY的最终目标不包括消除所有测试代码吗?

Answers:


25

我认为这是我能想到的任何一种误解。

测试生产代码的测试代码根本不相似。我将在python中演示:

def multiply(a, b):
    """Multiply ``a`` by ``b``"""
    return a*b

那么一个简单的测试将是:

def test_multiply():
    assert multiply(4, 5) == 20

这两个函数的定义相似,但功能却大不相同。这里没有重复的代码。;-)

人们还会写出重复的测试,每个测试功能本质上只有一个断言。这太疯狂了,我见过有人这样做。这不好的做法。

def test_multiply_1_and_3():
    """Assert that a multiplication of 1 and 3 is 3."""
    assert multiply(1, 3) == 3

def test_multiply_1_and_7():
    """Assert that a multiplication of 1 and 7 is 7."""
    assert multiply(1, 7) == 7

def test_multiply_3_and_4():
    """Assert that a multiplication of 3 and 4 is 12."""
    assert multiply(3, 4) == 12

想象一下,这样做需要1000多个有效代码行。相反,您可以按“功能”进行测试:

def test_multiply_positive():
    """Assert that positive numbers can be multiplied."""
    assert multiply(1, 3) == 3
    assert multiply(1, 7) == 7
    assert multiply(3, 4) == 12

def test_multiply_negative():
    """Assert that negative numbers can be multiplied."""
    assert multiply(1, -3) == -3
    assert multiply(-1, -7) == 7
    assert multiply(-3, 4) == -12

现在,当添加/删除功能时,我只需要考虑添加/删除一个测试功能。

您可能已经注意到我没有应用for循环。这是因为重复一些事情是好的。当我应用循环时,代码会短很多。但是,当断言失败时,它可能会混淆显示模糊消息的输出。如果发生这种情况,那么您的测试将不太有用,并且您需要调试器来检查哪里出错了。


8
技术上建议每个测试一个断言,因为这意味着多个问题不会仅显示为单个故障。但是,在实践中,我认为对断言的仔细汇总会减少重复代码的数量,并且我几乎从不坚持每个测试准则中的一个断言。
罗伯·丘奇

@ pink-diamond-square我看到断言失败后NUnit不会停止测试(我认为这很奇怪)。在这种情况下,每个测试最好有一个断言。如果单元测试框架在失败的断言后确实停止测试,则多个断言会更好。
siebz0r 2014年

3
NUnit不会停止整个测试套件,但是除非您采取措施阻止它,否则一个测试会停止(您可以捕获它引发的异常,这有时很有用)。我认为他们要说的是,如果编写的测试包含多个断言,您将无法获得纠正问题所需的全部信息。为了遍历您的示例,请想象这个乘法函数不喜欢数字3。在这种情况下,assert multiply(1,3)它将失败,但是您也不会获得关于的失败测试报告assert multiply(3,4)
罗伯·丘奇

我只是想提出它,因为从我在.net领域中所读的内容来看,每个测试有一个断言是“良好实践”,而多个断言是“务实用法”。在该示例执行两个断言的Python文档中,它看起来有些不同def test_shuffle
罗伯·丘奇

我同意和不同意:D这里显然存在重复:assert multiply(*, *) == *因此您可以定义一个assert_multiply函数。在当前情况下,行数和可读性无关紧要,但是通过更长的测试,您可以重用复杂的断言,固定装置,固定装置生成代码等...我不知道这是否是最佳实践,但我通常会这样做这个。
inf3rno 2014年

10

看来测试基本上表达了与代码相同的东西,因此是重复的

不,这不是真的。

测试的目的与实现的目的不同:

  • 测试确保您的实现有效。
  • 它们充当文档:通过查看测试,您可以看到代码必须满足的契约,即,哪些输入返回什么输出,什么是特殊情况等。
  • 此外,您的测试可确保在添加新功能时,现有功能不会中断。

4

否。DRY仅需编写一次代码即可完成特定任务,请测试该任务是否正确完成。这有点类似于投票算法,显然使用相同的代码将毫无用处。


2

DRY的最终目标不包括消除所有测试代码吗?

不,DRY的最终目标实际上意味着消除所有生产代码

如果我们的测试可以完美地说明我们希望系统执行的操作,则只需要自动生成相应的生产代码(或二进制文件)即可,从而有效地删除了生产代码库本身。

实际上,这就是模型驱动架构所声称的方法所要实现的-单个人工设计的真理源,一切都是通过计算得出的。

我认为相反(摆脱所有测试)不是可取的,因为:

  • 您必须解决实施与规范之间的阻抗不匹配问题。生产代码可以在一定程度上传达意图,但是要对表现良好的测试进行推理从来就没有那么容易。我们人类需要更高的观点来理解为什么要构建事物。即使您由于DRY而没有进行测试,规范也可能仍必须记录在文档中,如果您问我,这在阻抗失配和代码失步方面绝对是更加危险的野兽。
  • 虽然生产代码可以很容易地从正确的可执行规范中得出(假设有足够的时间),但是测试套件很难从程序的最终代码中重构。仅仅查看代码并不能清楚地显示出规范,因为在运行时很难区分代码单元之间的交互。这就是为什么我们很难处理无法测试的旧应用程序。换句话说:如果您希望您的应用程序能够生存几个月以上,那么最好丢掉承载生产代码库的硬盘,而不是测试套件所在的硬盘。
  • 在生产代码中偶然引入错误比在测试代码中引入错误要容易得多。而且,由于生产代码不是自动验证的(尽管可以通过按合同设计或更丰富的类型的系统来实现),所以我们仍然需要一些外部程序对其进行测试,并在发生回归时向我们发出警告。

1

因为有时候重复一遍是可以的。在任何情况下都不应毫无疑问地使用这些原则。我有时针对纯朴(缓慢)的算法版本进行测试,这是对DRY的明确明确的违反,但绝对是有益的。


1

由于单元测试的目的在于使无意更改变得更加困难,因此有时它也可能使有意更改变得更加困难。这个事实确实与DRY原则有关。

例如,如果您有一个MyFunction仅在生产代码中被调用的函数,并且为其编写了20个单元测试,则很容易最终在代码中有21个位置被调用该函数。现在,当您必须更改MyFunction,或或两者的签名(由于某些要求更改)时,您有21个要更改的地方,而不仅仅是一个。原因确实是违反了DRY原理的:您将(至少)相同的函数调用重复了MyFunction21次。

在这种情况下,正确的方法是也将DRY原理应用于您的测试代码:编写20个单元测试时,将对单元测试的调用封装MyFunction在仅几个帮助函数(最好是一个)中, 20个单元测试。理想情况下,您在代码调用中只剩下两个位置MyFunction:一个来自生产代码,另一个来自单元测试。因此,当您以后必须更改签名时MyFunction,测试中只有几个地方需要更改。

“几个地方”仍然比“一个地方”要多(根本没有任何单元测试的情况下),但是进行单元测试的好处应大大超过无需更改代码的好处(否则您将完全进行单元测试)错误)。


0

构建软件的最大挑战之一是捕获需求。那就是回答“此软件应该做什么?”的问题。 软件需要精确的需求来准确定义系统需要做什么,但是那些定义了软件系统和项目需求的人通常包括没有软件或正式(数学)背景的人员。需求定义缺乏严格性,迫使软件开发人员找到一种方法来验证软件是否符合需求。

开发团队发现自己将项目的口语描述转换为更严格的要求。测试学科已合并为软件开发的检查点,以弥合客户所说的需求与软件理解的需求之间的鸿沟。软件开发人员和质量/测试团队都形成对(非正式)规范的理解,并且每个(独立地)编写软件或测试以确保他们的理解是一致的。增加另一个人来理解(不精确的)需求会增加问题和不同的观点,以进一步磨练需求的准确性。

由于一直存在验收测试,因此自然而然地将测试角色扩展为编写自动化和单元测试。问题在于这意味着要雇用程序员进行测试,因此您将视野从质量保证缩小到了进行测试的程序员。

综上所述,如果您的测试与实际程序相差不大,则可能是测试错误。姆斯迪建议将重点更多地放在测试中,而不是如何。

具有讽刺意味的是,业界没有从口语描述中捕获需求的正式规范,而是选择将点测试作为实现自动化测试的代码来实现。与其提出可以构建软件的形式化要求,不如采用正式的方法来测试一些问题,而不是采用形式逻辑来构建软件。这是一个折衷方案,但是已经相当有效并且相对成功。


0

如果您认为测试代码与实现代码过于相似,则可能表明您过度使用了模拟框架。太低的基于模拟的测试最终可能会使测试设置看起来很像被测试的方法。尝试编写更高级的测试,如果您更改实现,则不太可能破坏测试(我知道这可能很困难,但是如果您可以进行管理,则将得到一个更有用的测试套件)。


0

正如已经指出的,单元测试不应包括被测代码的重复。

不过,我要补充一点,单元测试通常不像“生产”代码那样干,因为在各个测试中,安装趋向于相似(但不完全相同)……尤其是当您要模拟的依赖项数量很多时/伪造。
当然可以将这种事情重构为一种常见的设置方法(或一组设置方法)...但是我发现这些设置方法往往具有较长的参数列表,并且比较脆弱。

所以务实。如果您可以在不影响可维护性的情况下合并设置代码,那么请务必这样做。但是,如果替代方法是一组复杂而脆弱的设置方法,则可以在测试方法中进行一些重复。

本地的TDD / BDD传播者这样说:
“您的生产代码应为DRY。但是可以使测试“潮湿”。


0

看起来测试基本上表达了与代码相同的内容,因此是代码的重复(从概念上讲,不是实现)。

这是不正确的,测试描述了用例,而代码描述了通过用例的算法,因此更为通用。通过TDD,您首先编写用例(可能基于用户故事),然后实现传递这些用例所需的代码。因此,您编写了一个小测试,一小段代码,然后在必要时进行重构以消除重复。这就是它的工作原理。

通过测试,也可以重复。例如,您可以重用固定装置,固定装置生成代码,复杂的断言等……我通常这样做是为了防止测试中出现错误,但我通常忘记先测试一个测试是否真的失败了,这真的会毁了一天,当您在半小时内寻找代码中的错误并且测试是错误的... xD

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.