一位同事正在审查有关某些字符串生成的一些单元测试代码,这引发了冗长的讨论。他们说预期的结果都应该硬编码,并担心我的许多测试用例都使用了被测试的内容。
可以说有一个简单的函数返回带有一些参数的字符串。
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将会失败。告诉deconstructed_method只关心日期,并且仍然会通过它的测试。仅当日期发生意外时,它才会失败。更改后只有少数测试失败,其他人已经可以很容易地准确地确定出了什么问题,我认为这是单元测试的全部要点。
我仍在第一份编程工作的第一个月之内。我的同事比我经验丰富。我对自己的信念是零,通常只接受别人的观点作为真理,但这对我来说意义更大。我理解他们的想法,即从实际结果中获得预期结果可能会很糟糕,但是我相信所有其他测试都可以构成通知测试网。涵盖了字符串格式,标记值和格式,以及检查任何不正确性的硬编码测试。
是否应该对每个测试的预期结果进行硬编码?在测试基础之后,使用实际结果告知预期结果是否很糟糕?
答案 0 :(得分:2)
设计测试用例时应考虑程序的要求。如果仅需要验证字符串的一部分,则仅验证字符串的该部分。如果整个字符串都需要验证,请完整验证字符串。通过单元测试应强烈表明已遵守所有可直接测试的要求。
如果有可能某个错误将怪异现象插入了您不希望看到的部分,则您的测试方法将无法捕获这些错误。如果那是可以接受的风险,那么您可以选择忍受这个机会,但是您必须认识到这种可能性并决定自己的承受能力。
答案 1 :(得分: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)
答案 2 :(得分:0)
在某个时间点上,我与检查您的代码的人达成了一致:使测试变得非常简单。同时,我想测试代码的每个低级部分,以具有完整的测试范围并进行TDD。
您已经确定的问题是,残酷的简单测试是重复性的,当您需要为新方案更改内容时,必须更改许多测试代码。
然后,我与一个比我认识的世界级程序员多二十年经验的人一起编码。他说:“您的测试过于重复,要对其进行重构以使其不那么脆弱”。我说:“我认为我的测试必须非常简单明了,这意味着我的代码需要重复”。他说:“不要将您的测试代码写成与您的生产代码不同,让它们保持DRY(不要重复自己)。”
然后,这提出了关于我的编程的一整套元问题。什么是足够的测试代码?什么是好的测试代码?
我最终意识到,当我编写许多残酷的简单且重复的测试时,与重构新代码相比,我花费了更多的时间进行重构测试。大量重复的测试代码很脆弱。它并没有消除错误,它增加了功能或消除了技术债务。在业务逻辑方面,更多的代码并没有更多的价值。同样,更冗长的测试代码在重构时也无济于事,成为“测试债务”。
这又引出了另一个重点:松散类型的语言,需要大量的单元测试以证明是正确的,需要大量的易碎和重复的测试。强类型语言(使编译器可以在静态语言中静态地告诉您有关逻辑错误的信息)意味着您必须编写更少的测试代码,并且代码不那么脆弱,以便可以更快地重构。使用松散类型的语言,您最终要编写大量的测试代码,以确保在运行时不会传递错误的类型。在强类型函数语言中,您只需要在运行时验证输入:编译器将验证您的代码是否有效。这样,您就可以编写一些高级测试,并确信它们可以正常工作。如果重构代码,则重构的测试较少。您已将问题标记为“与语言无关”,但答案不能是。您的编译器越弱,这个问题就越多:您的编译器越强大,则处理整个问题的次数就越少。
我在Smalltalk的一家大型软件工程商店参加了为期四天的测试驱动开发课程。为什么?因为没有人知道小谈话,而且没有类型,所以我们都是初学者,因此我们必须为编写的所有内容编写测试。这很有趣,但是我不建议任何人使用松散类型的语言,因为他们不得不编写大量测试才能知道它的工作原理。我强烈建议人们使用强类型语言,在这种语言中,编译器会做更多的工作,并且测试代码可能会更少,因为当您添加新功能时,重构测试就更加容易。同样,具有不变代数类型和函数组成的函数式语言也不需要进行太多测试,因为它们不需要担心太多可变状态。编程语言越现代,为了避免错误,您需要编写的测试代码就越少。
很显然,您不能升级公司使用的语言。因此,这是我朋友说的一个建议:测试代码应该像生产代码一样,所以不要重复自己。如果发现测试正在重复,则删除测试。保持最小数量的测试,如果逻辑被破坏,这些测试将被破坏。不要保留涵盖字符串连接所有变体的五十个奇数测试。那就是“过度测试”过度测试会抑制重构,以增加功能并消除技术欠债,而这无法消除错误。在某些语言中,这意味着您需要编写许多重复测试,以验证逻辑是否为脚手架。然后,当您可以使用它时,编写更大的测试,如果某人破坏了一个子部件,则该测试将中断,并删除所有重复测试,以免留下“测试债务”。然后,这导致了一些粗粒度的测试,这些测试非常残酷,没有太多重复。