Answers:
我认为这是我能想到的任何一种误解。
测试生产代码的测试代码根本不相似。我将在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
循环。这是因为重复一些事情是好的。当我应用循环时,代码会短很多。但是,当断言失败时,它可能会混淆显示模糊消息的输出。如果发生这种情况,那么您的测试将不太有用,并且您将需要调试器来检查哪里出错了。
assert multiply(1,3)
它将失败,但是您也不会获得关于的失败测试报告assert multiply(3,4)
。
assert multiply(*, *) == *
因此您可以定义一个assert_multiply
函数。在当前情况下,行数和可读性无关紧要,但是通过更长的测试,您可以重用复杂的断言,固定装置,固定装置生成代码等...我不知道这是否是最佳实践,但我通常会这样做这个。
DRY的最终目标不包括消除所有测试代码吗?
不,DRY的最终目标实际上意味着消除所有生产代码。
如果我们的测试可以完美地说明我们希望系统执行的操作,则只需要自动生成相应的生产代码(或二进制文件)即可,从而有效地删除了生产代码库本身。
实际上,这就是模型驱动架构所声称的方法所要实现的-单个人工设计的真理源,一切都是通过计算得出的。
我认为相反(摆脱所有测试)不是可取的,因为:
由于单元测试的目的在于使无意更改变得更加困难,因此有时它也可能使有意更改变得更加困难。这个事实确实与DRY原则有关。
例如,如果您有一个MyFunction
仅在生产代码中被调用的函数,并且为其编写了20个单元测试,则很容易最终在代码中有21个位置被调用该函数。现在,当您必须更改MyFunction
,或或两者的签名(由于某些要求更改)时,您有21个要更改的地方,而不仅仅是一个。原因确实是违反了DRY原理的:您将(至少)相同的函数调用重复了MyFunction
21次。
在这种情况下,正确的方法是也将DRY原理应用于您的测试代码:编写20个单元测试时,将对单元测试的调用封装MyFunction
在仅几个帮助函数(最好是一个)中, 20个单元测试。理想情况下,您在代码调用中只剩下两个位置MyFunction
:一个来自生产代码,另一个来自单元测试。因此,当您以后必须更改签名时MyFunction
,测试中只有几个地方需要更改。
“几个地方”仍然比“一个地方”要多(根本没有任何单元测试的情况下),但是进行单元测试的好处应大大超过无需更改代码的好处(否则您将完全进行单元测试)错误)。
构建软件的最大挑战之一是捕获需求。那就是回答“此软件应该做什么?”的问题。 软件需要精确的需求来准确定义系统需要做什么,但是那些定义了软件系统和项目需求的人通常包括没有软件或正式(数学)背景的人员。需求定义缺乏严格性,迫使软件开发人员找到一种方法来验证软件是否符合需求。
开发团队发现自己将项目的口语描述转换为更严格的要求。测试学科已合并为软件开发的检查点,以弥合客户所说的需求与软件理解的需求之间的鸿沟。软件开发人员和质量/测试团队都形成对(非正式)规范的理解,并且每个(独立地)编写软件或测试以确保他们的理解是一致的。增加另一个人来理解(不精确的)需求会增加问题和不同的观点,以进一步磨练需求的准确性。
由于一直存在验收测试,因此自然而然地将测试角色扩展为编写自动化和单元测试。问题在于这意味着要雇用程序员进行测试,因此您将视野从质量保证缩小到了进行测试的程序员。
综上所述,如果您的测试与实际程序相差不大,则可能是测试错误。姆斯迪建议将重点更多地放在测试中,而不是如何。
具有讽刺意味的是,业界没有从口语描述中捕获需求的正式规范,而是选择将点测试作为实现自动化测试的代码来实现。与其提出可以构建软件的形式化要求,不如采用正式的方法来测试一些问题,而不是采用形式逻辑来构建软件。这是一个折衷方案,但是已经相当有效并且相对成功。
正如已经指出的,单元测试不应包括被测代码的重复。
不过,我要补充一点,单元测试通常不像“生产”代码那样干,因为在各个测试中,安装趋向于相似(但不完全相同)……尤其是当您要模拟的依赖项数量很多时/伪造。
当然可以将这种事情重构为一种常见的设置方法(或一组设置方法)...但是我发现这些设置方法往往具有较长的参数列表,并且比较脆弱。
所以务实。如果您可以在不影响可维护性的情况下合并设置代码,那么请务必这样做。但是,如果替代方法是一组复杂而脆弱的设置方法,则可以在测试方法中进行一些重复。
本地的TDD / BDD传播者这样说:
“您的生产代码应为DRY。但是可以使测试“潮湿”。
看起来测试基本上表达了与代码相同的内容,因此是代码的重复(从概念上讲,不是实现)。
这是不正确的,测试描述了用例,而代码描述了通过用例的算法,因此更为通用。通过TDD,您首先编写用例(可能基于用户故事),然后实现传递这些用例所需的代码。因此,您编写了一个小测试,一小段代码,然后在必要时进行重构以消除重复。这就是它的工作原理。
通过测试,也可以重复。例如,您可以重用固定装置,固定装置生成代码,复杂的断言等……我通常这样做是为了防止测试中出现错误,但我通常忘记先测试一个测试是否真的失败了,这真的会毁了一天,当您在半小时内寻找代码中的错误并且测试是错误的... xD