【问题标题】:Is it bad practice to base expected results off actual results in unit testing?在单元测试中将预期结果基于实际结果是不好的做法吗?
【发布时间】:2019-04-09 19:34:46
【问题描述】:

一位同事正在审查我的一些关于某个字符串生成的单元测试代码,这引发了一场冗长的讨论。他们说预期的结果都应该是硬编码的,并且担心我的很多测试用例都在使用正在测试的东西来进行测试。

假设有一个简单的函数返回带有一些参数的字符串。

generate_string(name, date) #  Function to test
    result 'My Name is {name} I was born on {date} and this isn't my first rodeo'

----Test----

setUp
    name = 'John Doe'
    date = '1990-01-01'

test_that_generate_string_function
    ...
    expected = 'My Name is John Doe I was born on 1990-01-01 and this isn't my first rodeo'
    assertEquals(expected, actual)

我的同事马上就认为预期结果应该始终是硬编码的,因为它不再有任何机会让实际结果影响预期结果。

test_date_hardcoded_method
    ...
    date = 1990-01-01
    actual = generate_string(name, date)
    expected = 'My Name is John Doe I was born on 1990-01-01 and this isn't my first rodeo'

因此,如果他们想确保日期完全符合要求,他们会传入一个日期值并对预期结果进行硬编码。对我来说,这是有道理的,但也似乎是多余的。该函数已经进行了测试,以确保整个字符串符合预期。任何偏离都会导致测试失败。我的方法是获取实际结果,对其进行解构,对特定内容进行硬编码,然后将其重新组合在一起以用作预期结果。

test_date_deconstucted_method
    ...
    date = get_date()
    actual = generate_string(name, date)
    actual_deconstructed = actual.split(' ')
    actual_deconstructed[-7] = '1990-01-01'  # Hard code small expected change
    expected = join.actual_deconstructed
    assertEquals(expected, actual)

我最终使用每种方法创建了两个测试单元,看看我是否能理解它们的来源,但我就是看不到。当所有预期结果都被硬编码时,任何微小的变化都会使绝大多数测试失败。如果“不是”需要是“不是”,hardcoed_method 将失败,直到有人手动更改内容。 Whist deconstructed_method 只关心日期并且仍然会通过它的测试。只有当日期发生意外情况时,它才会失败。在其他人进行更改后只有少数测试失败,因此很容易准确地确定出了什么问题,我认为这就是单元测试的全部意义所在。

我还处于我第一份编程工作的第一个月内。我的同事比我更有经验。我对自己的信念为零,通常只接受别人的意见作为真理,但这对我来说更有意义。我理解他们的想法,即从实际结果中获取预期结果可能很糟糕,但我相信所有其他测试会形成一个通知测试网络。字符串格式、标记值和格式都包括在内,以及检查任何不正确的硬编码测试。

是否应该对每个测试的预期结果进行硬编码?一旦基础工作已经过测试,使用实际结果来告知预期结果是否不好?

【问题讨论】:

  • 对于标题Is it bad practice to base expected results off actual results in unit testing?,我的回答是肯定的,这是不好的做法。
  • For My co-worker was instant that the expected result should always be hard-coded, as it stops there being any chance that the actual result can influence the expected result. 听起来你在转述。我并不总是对我的结果进行硬编码,而是使用生成测试和结果的生成器,它不使用被测试代码中的任何方法或基本方法。这通常很困难,因为很多时候我不得不跳过铁环重新发明轮子,当我想不出办法时,我会对结果进行硬编码。
  • 对于The function already has a test to make sure the entire string is as expected. 编程和证明不是一回事。很少有程序能够证明某些事情。对于I trust all the other tests to form a web of informing tests;相信我,我有一个bridge for sale

标签: unit-testing language-agnostic hardcode


【解决方案1】:

您的测试用例的设计应考虑到程序的要求。如果只需要验证字符串的一部分,则只验证字符串的那一部分。如果整个字符串需要验证,请完整验证字符串。通过单元测试应该强烈表明所有可直接测试的需求都已得到遵守。

如果错误有可能在您未查看的部分中插入了奇怪的内容,那么您的测试方法将无法捕捉到这些错误。如果这是一个可接受的风险,那么您可以选择接受这个机会,但您必须认识到这种可能性并决定自己的承受能力。

【讨论】:

  • you have to recognize the possibility and decide your own tolerance. 在银行或安全地点尝试这种理念,看看你有多久的工作。公司或客户设定要求,如有疑问,请咨询。
  • @GuyCoder 哈哈,你说得对。也许you 无法决定你的容忍度,但有人会。我习惯于在一家初创公司工作,也许不幸的是,我可以做出这些决定。我认为即使对于大型银行也可以接受较少验证的一个领域是 UI/UX 设计,其中最坏的情况是渲染效果不佳。给出的示例看起来有点像前端工作,这就是为什么我觉得他的需求可能有些放松。
【解决方案2】:

您有一个从输入数据生成字符串的函数。可以选择让测试用例始终测试整个生成的字符串,尽管每个测试的测试目标是验证该字符串的特定部分。您认为这种方法不好是正确的:生成的测试将过于广泛,因此很脆弱。它们将失败/必须针对任何更改进行维护,不仅是在更改影响生成字符串的特定部分的情况下。看看 Meszaros 对脆弱测试的讨论,你可能会发现它很有启发性,特别是“测试说明了软件应该如何构建或行为”的部分:http://xunitpatterns.com/Fragile%20Test.html#Overspecified%20Software

实际上,更好的解决方案是让您的测试更加专注,就像您希望它们那样。但是,您选择的方法有点奇怪:您获取生成的字符串,制作一个副本,使用手动编码的预期字符串部分修补副本,该部分在相应测试中是焦点,然后再次比较两个完整的字符串,结果和您的修补结果。从技术上讲,您已经创建了一个真正只关注预期部分的测试,因为围绕它的字符串的其他部分将始终相同。然而,这种方法令人困惑:对于不完全理解测试代码的人来说,似乎您是根据代码本身的结果来测试代码。

你为什么不反过来做:取出结果字符串,剪下感兴趣的部分,并将这部分与硬编码的期望进行比较?在您的示例中,测试将如下所示:

test_date_part_of_generated_string:
   date = 1990-01-01
   actual_full_string = generate_string(name, date)
   actual_string_parts = actual_full_string.split(' ')
   actual_date_part = actual_string_parts[-7]
   assertEquals('1990-01-01', actual_date_part)

【讨论】:

    【解决方案3】:

    在某个时间点,我同意审查您的代码的人的观点:让测试变得非常简单。同时,我想测试代码的每一个低级部分,以获得完整的测试覆盖率并进行 TDD。

    正如您所发现的,问题在于极其简单的测试是重复性的,当您需要针对新场景进行更改时,您必须更改大量测试代码。

    然后我和一个比我认识的世界级程序员有 20 多年经验的人一起编码。他说“你的测试太重复了,重构它们以降低它们的脆弱性”。我说“我认为我的测试需要非常简单和明显,这意味着我的代码需要重复”。他说:“不要将测试代码编写成与生产代码有任何不同,让它们保持干燥(不要重复自己)”。

    这引发了一整类关于我的职业生涯的元问题。什么是足够的测试代码?什么是好的测试代码?

    我最终意识到,当我编写大量极其简单和重复的测试时,我花在重构测试上的时间比编写新代码的时间还要多。大量重复测试代码很脆弱。它并没有阻止错误,它使添加功能或消除技术债务变得更加困难。当涉及到业务逻辑时,更多的代码并没有更多的价值。同样,更冗长的测试代码在重构时也无济于事,成为“测试债务”。

    这就引出了另一个重点:松散类型的语言,需要大量单元测试来证明是正确的,需要大量脆弱和重复的测试。强类型语言,编译器可以静态地告诉你逻辑错误,这意味着你必须编写更少的测试代码,也就是不那么脆弱,这样你就可以更快地重构。在松散类型的语言中,您最终会编写大量测试代码,以确保在运行时不会传递错误的类型。在强类型函数语言中,您只需要在运行时验证输入:编译器会验证您的代码是否有效。因此,您可以编写一些高级测试,并确信一切正常。如果你重构你的代码,你需要重构的测试更少。您已将您的问题标记为“与语言无关”,但答案不能。你的编译器越弱,这个问题就越是一个问题:你的编译器越强大,你处理整个问题的次数就越少。

    我在一家大型软件工程商店参加了为期四天的测试驱动开发课程,该课程是在 Smalltalk 中完成的。为什么?因为没有人知道 smalltalk,而且它是无类型的,所以我们必须为我们写的每一件事写一个测试,因为我们都是该语言的初学者。这很有趣,但我不建议任何人使用松散类型的语言,他们必须编写大量测试才能知道它是否有效。我强烈建议人们使用强类型语言,其中编译器做更多的工作,测试代码可以更少,因为当你添加新功能时更容易重构测试。同样,具有不可变代数类型和函数组合的函数式语言需要较少的测试,因为它们不需要担心很多可变状态。编程语言越现代,为避免错误而需要编写的测试代码就越少。

    显然,您无法升级您在公司使用的语言。所以这是我朋友说的一个让我坚持的提示:测试代码应该像生产代码一样,所以不要重复自己。如果您发现您的测试变得重复,则删除测试。保持最少数量的测试,如果逻辑被破坏,就会破坏。不要保留涵盖字符串连接所有变体的五十多个测试。那就是“过度测试” 过度测试抑制了重构以添加功能和消除技术债务,而不是阻止错误。在某些语言中,这意味着编写大量重复的测试,您需要在将其编写为脚手架时验证您的逻辑。然后当你让它工作时,编写更大的测试,如果有人破坏了一个子部分并删除所有重复的测试,这样就会破坏,以免留下“测试债务”。然后,这会导致一些粗粒度的测试,这些测试非常简单,无需大量重复。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-07-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多