一段时间以前,我破坏了几个单元测试,当时我对它们进行了重构,使它们更像 干的——每个测试的意图不再清楚。似乎在测试的可读性和可维护性之间存在权衡。如果我在单元测试中保留重复的代码,它们就更具可读性,但是如果我更改 SUT,我就必须跟踪并更改重复代码的每个副本。
您同意存在这种权衡吗? 如果存在,您是希望您的测试是可读的还是可维护的?
可读性对于测试来说更为重要。如果测试失败,您希望问题显而易见。开发人员不应该费力地通过大量重要的测试代码来确定哪些代码失败了。您不希望您的测试代码变得如此复杂,以至于您需要编写单元测试测试。
然而,消除重复通常是一件好事,只要它不模糊任何东西,并且消除测试中的重复可能导致更好的 API。只要确保你不会越过报酬递减。
我不认为更多的重复代码和可读代码之间有什么联系。我认为您的测试代码应该和其他代码一样好。非重复代码的可读性比重复代码做得好时更好。
理想情况下,单元测试一旦编写就不会有太大变化,所以我倾向于可读性。
让单元测试尽可能离散也有助于使测试集中于它们所针对的特定功能。
说到这里,我确实倾向于尝试和重用某些代码片段,这些代码是我反复使用的,比如在一组测试中完全相同的设置代码。
我同意。权衡是存在的,但在不同的地方是不同的。
我更倾向于重构重复的代码来设置状态。但是不太可能重构实际执行代码的测试部分。也就是说,如果测试代码总是需要几行代码,那么我可能会认为这是一种味道,并重构测试中的实际代码。这将提高代码和测试的可读性和可维护性。
实现代码和测试是不同的动物,因子分析规则对它们的应用也不同。
在实现代码中,重复的代码或结构总是一种味道。当您开始在实现中使用样板时,您需要修改您的抽象。
另一方面,测试代码必须保持一定程度的重复:
我倾向于忽略测试代码中的微不足道的重复,只要每个测试方法保持短于20行。我喜欢当安装-运行-验证节奏是明显的测试方法。
当测试的“验证”部分出现重复时,定义自定义断言方法通常是有益的。当然,这些方法仍然必须测试一个可以在方法名称中显示出来的明确标识的关系: assertPegFitsInHole-> good,assertPegIsGood-> bad。
assertPegFitsInHole
assertPegIsGood
当测试方法变得冗长和重复时,我有时发现定义带有一些参数的填充测试模板很有用。然后,将实际的测试方法简化为对具有适当参数的模板方法的调用。
对于编程和测试中的许多问题,没有明确的答案。你需要培养品味,而最好的方法就是犯错误。
您可以使用几种不同风味的 试验实用方法试验实用方法来减少重复。
我对测试代码中的重复比对产品代码中的重复更加宽容,但是我有时会因此而感到沮丧。当您更改一个类的设计时,您必须返回并调整10个不同的测试方法,这些方法都执行相同的设置步骤,这是令人沮丧的。
“重构它们使它们更干燥——每个测试的目的不再清楚”
听起来你在重构方面遇到了麻烦。我只是猜测,但如果结果不那么清楚,是不是意味着你还有更多的工作要做,这样你就有了相当优雅的测试,而且完全清楚?
这就是为什么测试是 UnitTest 的一个子类——因此您可以设计正确、易于验证和清除的好的测试套件。
在过去,我们有使用不同编程语言的测试工具。很难(或者不可能)设计出令人愉快的、易于使用的测试。
无论你使用什么语言,Python,Java,C # ,你都拥有完全的能力,所以要好好地使用这种语言。您可以实现清晰且不太多余的好看的测试代码。没有交易。
我喜欢 Rspec 就是因为这个:
它有两个方面的帮助-
用于测试常见行为的共享示例组。 您可以定义一组测试,然后“ include”在您的实际测试中设置
嵌套上下文。 实际上,您可以为您的测试的特定子集(而不仅仅是类中的每一个)提供一个“ setup”和“ teardown”方法。
越快越好。NET/Java/其他测试框架采用这些方法,效果会更好(或者您可以使用 IronRuby 或 JRuby 来编写测试,我个人认为这是更好的选择)
复制的代码在单元测试代码中和在其他代码中一样是一种气味。如果测试中有重复的代码,那么重构实现代码就会变得更加困难,因为要更新的测试数量不成比例。测试应该帮助您自信地重构代码,而不是成为阻碍您对正在测试的代码进行工作的巨大负担。
如果复制是在夹具设置,考虑更多地使用 setUp方法或提供更多(或更灵活) 创作方法。
setUp
如果复制在操作 SUT 的代码中,那么问问自己为什么多个所谓的“单元”测试正在执行完全相同的功能。
如果断言中存在重复,那么可能需要一些 定制断言。例如,如果多个测试具有如下断言字符串:
assertEqual('Joe', person.getFirstName()) assertEqual('Bloggs', person.getLastName()) assertEqual(23, person.getAge())
然后,您可能需要一个单独的 assertPersonEqual方法,这样就可以编写 assertPersonEqual(Person('Joe', 'Bloggs', 23), person)。(或者您只需要重载 Person上的等式运算符。)
assertPersonEqual
assertPersonEqual(Person('Joe', 'Bloggs', 23), person)
Person
正如您所提到的,测试代码的可读性非常重要。特别是,测试的 意图是清晰的,这一点很重要。我发现,如果许多测试看起来大致相同(例如,四分之三的行相同或几乎相同) ,那么如果不仔细阅读和比较,就很难发现和识别出显著的差异。所以我发现重构可以去除重复的 有帮助可读性,因为每一个测试方法的每一行都直接关系到测试的目的。对于读者来说,这比直接相关的行和只是样板的行的随机组合要有用得多。
也就是说,有时候测试所测试的复杂情况相似,但仍然存在显著差异,因此很难找到减少重复的好方法。使用常识: 如果你觉得测试是可读的,并且它们的意图很清晰,而且在重构测试调用的代码时,你可能需要更新超过理论上最小数量的测试,那么接受这些不完美,然后转向更有效率的事情。当灵感来袭时,您随时可以回来重构测试!
Jay Fields 创造了这个短语“ DSL 应该是 DAMP,而不是 DRY”,其中 潮湿的意思是 描述性和有意义的短语。我认为这同样适用于测试。显然,太多的重复是不好的。但不惜一切代价消除重复更糟糕。测试应该作为揭示意图的规范。例如,如果您从几个不同的角度指定了相同的特性,那么就会出现一定数量的重复。
我觉得测试代码需要类似的工程水平,通常应用于生产代码。当然也有支持可读性的论据,我也同意这一点很重要。
然而,根据我的经验,我发现构造良好的测试更容易阅读和理解。如果有5个测试,除了一个变量和最后的断言之外,每个测试看起来都一样,那么就很难找到那个单独的不同项是什么。类似地,如果它被分解成只有变量和断言是可见的,那么就很容易知道测试立即在做什么。
当测试很困难的时候,找到正确的抽象级别,我觉得这是值得做的。