重复代码在单元测试中是否更容易被容忍?

时间:2008-09-24 20:22:11

标签: unit-testing dry code-duplication

前一段时间我破坏了几次单元测试,当时我经历并重构它们以使它们更加DRY - 每次测试的意图都不再明确。似乎在测试的可读性和可维护性之间存在权衡。如果我在单元测试中留下重复的代码,它们会更具可读性,但如果我更改SUT,我将不得不追踪并更改重复代码的每个副本。

你是否同意这种权衡存在?如果是这样,您是否希望您的测试具有可读性或可维护性?

11 个答案:

答案 0 :(得分:163)

可读性对于测试更重要。如果测试失败,您希望问题显而易见。开发人员不应该花费大量重要的测试代码来确定失败的确切内容。您不希望您的测试代码变得如此复杂,以至于您需要编写单元测试测试。

然而,消除重复通常是一件好事,只要它不会掩盖任何内容,并且消除测试中的重复可能会导致更好的API。只要确保你没有超过收益递减点。

答案 1 :(得分:63)

重复代码是单元测试代码中的气味,就像在其他代码中一样。如果您在测试中有重复的代码,则会使重构实现代码变得更加困难,因为您需要更新不成比例的测试。测试应该可以帮助您充满信心地进行重构,而不是阻碍您对所测试代码进行工作的巨大负担。

如果重复设置在夹具设置中,请考虑更多地使用setUp方法或提供更多(或更灵活)Creation Methods

如果复制是在操作SUT的代码中,那么问问自己为什么多个所谓的“单元”测试正在执行完全相同的功能。

如果重复在断言中,那么您可能需要一些Custom Assertions。例如,如果多个测试有一串断言,如:

assertEqual('Joe', person.getFirstName())
assertEqual('Bloggs', person.getLastName())
assertEqual(23, person.getAge())

那么也许您需要一个assertPersonEqual方法,以便您可以编写assertPersonEqual(Person('Joe', 'Bloggs', 23), person)。 (或者您可能只需要在Person上重载相等运算符。)

正如您所提到的,测试代码的可读性非常重要。特别是,测试的 intent 非常重要。我发现,如果许多测试看起来大致相同(例如四分之三的线条相同或几乎相同),则很难发现并识别出显着的差异而不仔细阅读和比较它们。所以我发现重构删除重复有助于可读性,因为每个测试方法的每一行都与测试的目的直接相关。对于读者而言,这比直接相关的随机组合线和仅仅是样板的线条更有帮助。

尽管如此,有时测试正在进行类似但仍然存在显着差异的复杂情况,并且很难找到减少重复的好方法。使用常识:如果您觉得测试是可读的并且使他们的意图清楚,并且您对在重构测试调用的代码时可能需要更新理论上最少数量的测试感到满意,那么接受不完美和移动更有成效的事情。当灵感来袭时,你总能回来重构测试!

答案 2 :(得分:37)

实施代码和测试是不同的动物,因子规则适用于它们。

重复的代码或结构在实现代码中始终是一种气味。当你开始实现样板时,你需要修改你的抽象。

另一方面,测试代码必须保持重复级别。测试代码中的重复实现了两个目标:

  • 保持测试分离。由于合同已经发生变化,过多的测试耦合会导致很难更改需要更新的单个故障测试。
  • 保持测试的独立性。当单个测试失败时,必须合理地直接找出它正在测试的内容。

我倾向于忽略测试代码中的琐碎重复,只要每个测试方法保持短于约20行。我喜欢在测试方法中设置运行验证节奏的时候。

当复制在测试的“验证”部分中逐渐增加时,定义自定义断言方法通常是有益的。当然,这些方法仍然必须测试一个明确标识的关系,这种关系可以在方法名称中明确表示:assertPegFitsInHole - >好的,assertPegIsGood - >坏。

当测试方法变长且重复时,我有时会发现定义填充空白的测试模板很有用,这些模板需要一些参数。然后将实际的测试方法简化为使用适当的参数调用模板方法。

对于编程和测试中的很多事情,没有明确的答案。你需要培养品味,最好的方法就是犯错误。

答案 3 :(得分:8)

您可以使用几种不同风格的test utility methods来减少重复。

我对测试代码中的重复比对生产代码的重复更宽容,但我有时会对它感到沮丧。当你改变一个类的设计时,你必须回过头来调整10个不同的测试方法,这些方法都做相同的设置步骤,这很令人沮丧。

答案 4 :(得分:7)

我同意。权衡存在但在不同的地方有所不同。

我更有可能重构重复代码来设置状态。但不太可能重构实际运行代码的测试部分。也就是说,如果执行代码总是需要几行代码,那么我可能会认为这是一种气味并重构了测试中的实际代码。这将提高代码和测试的可读性和可维护性。

答案 5 :(得分:4)

Jay Fields创造了“DSL应该是DAMP而不是干”的短语,其中 DAMP 意味着描述性和有意义的短语。我认为同样适用于测试。显然,重复太多是不好的。但不惜一切代价去除重复甚至更糟。测试应该作为明确揭示的规范。例如,如果您从几个不同的角度指定相同的特征,则需要一定量的重复。

答案 6 :(得分:3)

我喜欢rspec因为:

它有两件事需要帮助 -

  • 用于测试常见行为的共享示例组 您可以定义一组测试,然后在实际测试中设置“包含”。

  • 嵌套环境。
    对于特定的测试子集,您基本上可以使用“设置”和“拆卸”方法,而不仅仅是班级中的每一个。

.NET / Java /其他测试框架越早采用这些方法越好(或者你可以使用IronRuby或JRuby编写测试,我个人认为这是更好的选择)

答案 7 :(得分:2)

我认为更多重复和可读代码之间没有关系。我认为您的测试代码应该与其他代码一样好。如果做得好,非重复代码比重复代码更易读。

答案 8 :(得分:2)

理想情况下,单元测试一旦写完就不应该改变太多,所以我倾向于可读性。

让单元测试尽可能离散也有助于使测试专注于他们所针对的特定功能。

话虽如此,我确实倾向于重复使用我反复使用的某些代码片段,例如在一组测试中完全相同的设置代码。

答案 9 :(得分:2)

我觉得测试代码需要通常应用于生产代码的类似工程级别。当然可以提出支持可读性的论据,我同意这一点很重要。

但是,根据我的经验,我发现经过充分考虑的测试更容易阅读和理解。如果有5个测试,每个测试看起来都是相同的,除了一个变化的变量和最后的断言,很难找到单个不同项目的内容。类似地,如果考虑到只有变化的变量是可见的和断言,那么很容易弄清楚测试在做什么。

在测试时找到合适的抽象级别可能很困难,我觉得值得做。

答案 10 :(得分:1)

“重构它们以使它们更干燥 - 每次测试的意图都不再明确”

听起来你在重构方面遇到了麻烦。我只是猜测,但是如果它不那么清楚,那是不是意味着你还有更多的工作要做,以便你有相当优雅的测试,这些测试非常清楚?

这就是测试是UnitTest的子类的原因 - 因此您可以设计出正确,易于验证和清晰的优秀测试套件。

在过去,我们有使用不同编程语言的测试工具。很难(或不可能)设计出令人愉快,易于操作的测试。

你有充分的力量 - 无论你使用什么语言 - Python,Java,C# - 所以要好好利用这种语言。您可以获得清晰且不太冗余的漂亮的测试代码。没有权衡。