将预期结果基于单元测试中的实际结果是不好的做法吗?

问题描述 投票:1回答:3

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

让我们说有一个简单的函数返回带有一些参数的字符串。

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只关心日期,仍然会通过它的测试。只有在日期发生意外情况时才会失败。只有少数测试在更改后失败,其他人已经让它很容易确切地指出出错的地方,我认为这是单元测试的全部要点。

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

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

unit-testing language-agnostic hardcode
3个回答
2
投票

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

如果错误有可能将怪异插入到您未查看的碎片中,则您的测试方法将无法捕获这些错误。如果这是一个可接受的风险,那么你可以选择忍受这种机会,但你必须认识到这种可能性并决定你自己的容忍度。


0
投票

您有一个从输入数据生成字符串的函数。可以选择让测试用例始终测试整个生成的字符串,尽管每个测试的测试目标是验证该字符串的一个非常特定的部分。你认为这种方法很糟糕是正确的:由此产生的测试太宽泛,因此很脆弱。它们将无法/必须维护以进行任何更改,不仅仅是在影响生成的字符串的特定部分的更改的情况下。您可能会发现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)

0
投票

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

正如您所知,问题在于粗暴简单的测试是重复的,当您需要为新场景更改内容时,您必须更改大量测试代码。

然后我和一个比我知道的世界级程序员经验长20年的人编码。他说“你的测试过于重复,重构它们以使它们不那么脆弱”。我说“我认为我的测试需要粗暴和明显,这意味着我的代码需要重复”。并且他说“不要写你的测试代码与你的生产代码有任何不同,让他们保持干燥(不要重复自己)”。

然后,这提出了关于我的编程的一整类元问题。什么是足够的测试代码?什么是好的测试代码?

我最终意识到的是,当我写了很多残酷简单和重复的测试时,我花了更多时间重构测试,而不是编写新代码。大量的重复测试代码很脆弱。它没有阻止它增加功能或更难减少技术债务。在业务逻辑方面,更多代码并不是更有价值。同样,更详细的测试代码在重构它变成“测试债务”时没有帮助。

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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.