【发布时间】:2008-09-24 20:22:10
【问题描述】:
前段时间我破坏了几个单元测试,当我经历并重构它们以使它们更多DRY--每个测试的意图不再明确。似乎测试的可读性和可维护性之间存在权衡。如果我在单元测试中留下重复的代码,它们的可读性会更高,但是如果我更改 SUT,我将不得不追踪并更改重复代码的每个副本。
您是否同意这种权衡存在?如果是这样,您更喜欢您的测试是可读的还是可维护的?
【问题讨论】:
标签: unit-testing dry code-duplication
前段时间我破坏了几个单元测试,当我经历并重构它们以使它们更多DRY--每个测试的意图不再明确。似乎测试的可读性和可维护性之间存在权衡。如果我在单元测试中留下重复的代码,它们的可读性会更高,但是如果我更改 SUT,我将不得不追踪并更改重复代码的每个副本。
您是否同意这种权衡存在?如果是这样,您更喜欢您的测试是可读的还是可维护的?
【问题讨论】:
标签: unit-testing dry code-duplication
可读性对于测试来说更为重要。如果测试失败,您希望问题显而易见。开发人员不必费力地通过大量因素分解的测试代码来确定究竟是什么失败了。您不希望您的测试代码变得如此复杂以至于您需要编写单元测试测试。
但是,消除重复通常是一件好事,只要它不会掩盖任何内容,并且消除测试中的重复可能会带来更好的 API。只要确保你没有超过收益递减点。
【讨论】:
重复代码在单元测试代码中与其他代码一样是一种气味。如果您在测试中有重复的代码,那么重构实现代码就会变得更加困难,因为您要更新的测试数量不成比例。测试应该帮助您自信地重构,而不是成为阻碍您在被测代码上工作的巨大负担。
如果重复是在夹具设置中,请考虑更多地使用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 测试的明确性很重要。我发现如果许多测试看起来基本相同(例如,四分之三的行相同或几乎相同),如果不仔细阅读和比较它们,就很难发现和识别显着差异。所以我发现重构以消除重复有助于可读性,因为每个测试方法的每一行都与测试的目的直接相关。这比直接相关的行和只是样板的行的随机组合对读者更有帮助。
也就是说,有时测试是在执行相似但仍然显着不同的复杂情况,并且很难找到减少重复的好方法。使用常识:如果你觉得测试是可读的并且他们的意图很清楚,并且你对在重构测试调用的代码时可能需要更新超过理论上最少数量的测试感到满意,那么接受不完美并移动去做更有成效的事情。当灵感来袭时,您可以随时回来重构测试!
【讨论】:
实现代码和测试是不同的动物,因式分解规则适用于它们。
重复的代码或结构在实现代码中总是有异味。当您开始在实现中使用样板时,您需要修改您的抽象。
另一方面,测试代码必须保持一定程度的重复。测试代码中的重复实现了两个目标:
我倾向于忽略测试代码中的琐碎重复,只要每个测试方法都保持在 20 行以内。我喜欢设置-运行-验证的节奏在测试方法中很明显。
当测试的“验证”部分出现重复时,定义自定义断言方法通常是有益的。当然,这些方法仍然必须测试可以在方法名称中明确识别的关系:assertPegFitsInHole -> 好,assertPegIsGood -> 坏。
当测试方法变得冗长且重复时,我有时会发现定义采用一些参数的填空测试模板很有用。然后将实际的测试方法简化为对具有适当参数的模板方法的调用。
对于编程和测试中的很多事情,并没有明确的答案。你需要培养一种品味,而做到这一点的最好方法就是犯错。
【讨论】:
我同意。权衡是存在的,但在不同的地方是不同的。
我更有可能重构重复的代码来设置状态。但不太可能重构实际执行代码的测试部分。也就是说,如果执行代码总是需要几行代码,那么我可能会认为这是一种异味并重构被测的实际代码。这将提高代码和测试的可读性和可维护性。
【讨论】:
您可以使用几种不同风格的test utility methods 来减少重复。
与生产代码相比,我更能容忍测试代码中的重复,但有时我对此感到沮丧。当你改变了一个类的设计,你必须回去调整 10 种不同的测试方法,它们都执行相同的设置步骤,这很令人沮丧。
【讨论】:
Jay Fields 创造了“DSL 应该是 DAMP,而不是 DRY”这一短语,其中 DAMP 表示 描述性和有意义的短语。我认为这同样适用于测试。显然,过多的重复是不好的。但不惜一切代价消除重复甚至更糟。测试应该作为意图揭示规范。例如,如果您从几个不同的角度指定相同的特征,那么会出现一定数量的重复。
【讨论】:
“重构它们以使它们更干燥——每个测试的意图不再清晰”
听起来你在重构时遇到了麻烦。我只是在猜测,但如果结果不太清楚,这是否意味着您还有更多工作要做,以便您拥有相当优雅且非常清楚的测试?
这就是为什么测试是 UnitTest 的子类——因此您可以设计正确、易于验证和清晰的良好测试套件。
在过去,我们有使用不同编程语言的测试工具。设计令人愉快、易于使用的测试很难(或不可能)。
无论您使用何种语言(Python、Java、C#),您都拥有强大的能力,因此请好好使用该语言。您可以获得清晰且不太冗余的漂亮测试代码。没有取舍。
【讨论】:
因为这个,我喜欢 rspec:
它有两件事可以帮助 -
用于测试常见行为的共享示例组。
您可以定义一组测试,然后在您的实际测试中“包含”该设置。
嵌套上下文。
您基本上可以为测试的特定子集使用“设置”和“拆卸”方法,而不仅仅是类中的每个测试。
.NET/Java/其他测试框架越早采用这些方法越好(或者你可以使用 IronRuby 或 JRuby 来编写你的测试,我个人认为这是更好的选择)
【讨论】:
我觉得测试代码需要与通常应用于生产代码类似的工程级别。当然可以有支持可读性的论据,我同意这很重要。
然而,根据我的经验,我发现精心设计的测试更容易阅读和理解。如果有 5 个测试,除了一个变量发生变化和最后的断言之外,每个测试看起来都一样,那么很难找到那个不同的项目是什么。类似地,如果考虑到只有变化的变量和断言是可见的,那么很容易立即弄清楚测试在做什么。
在测试可能很困难时找到正确的抽象级别,我认为这是值得的。
【讨论】:
我认为更多重复和可读的代码之间没有关系。我认为你的测试代码应该和你的其他代码一样好。如果做得好,非重复代码比重复代码更具可读性。
【讨论】:
理想情况下,单元测试在编写后不应有太大变化,因此我倾向于可读性。
让单元测试尽可能离散也有助于让测试专注于他们所针对的特定功能。
话虽如此,我确实倾向于尝试并重用我反复使用的某些代码,例如在一组测试中完全相同的设置代码。
【讨论】: