127

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

你同意这种权衡存在吗?如果是这样,你更喜欢你的测试是可读的还是可维护的?

4

11 回答 11

214

可读性对于测试来说更为重要。如果测试失败,您希望问题显而易见。开发人员不必费力地通过大量因素分解的测试代码来确定究竟是什么失败了。您不希望您的测试代码变得如此复杂以至于您需要编写单元测试测试。

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

于 2008-09-24T20:25:01.700 回答
72

与其他代码一样,重复代码在单元测试代码中是一种气味。如果您在测试中有重复的代码,那么重构实现代码就会变得更加困难,因为您要更新的测试数量不成比例。测试应该帮助您自信地重构,而不是成为阻碍您在被测代码上工作的巨大负担。

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

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

如果重复在断言中,那么也许您需要一些自定义断言。例如,如果多个测试有一系列断言,例如:

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

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

正如您所提到的,测试代码的可读性很重要。尤其重要的是,测试的意图要明确。我发现如果许多测试看起来大体相同,(例如,四分之三的行相同或几乎相同),如果不仔细阅读和比较它们,就很难发现和识别显着差异。所以我发现重构以消除重复有助于提高可读性,因为每个测试方法的每一行都与测试的目的直接相关。这比直接相关的行和只是样板的行的随机组合对读者更有帮助。

也就是说,有时测试是在执行相似但仍然显着不同的复杂情况,并且很难找到减少重复的好方法。使用常识:如果你觉得测试是可读的并且他们的意图很清楚,并且你对在重构测试调用的代码时可能需要更新超过理论上最少数量的测试感到满意,那么接受不完美并移动去做更有成效的事情。当灵感来袭时,您可以随时回来重构测试!

于 2008-09-27T14:26:56.707 回答
54

实现代码和测试是不同的动物,因式分解规则适用于它们不同。

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

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

  • 保持测试解耦。过度的测试耦合可能会导致很难更改单个失败的测试,因为合同已经更改,需要更新。
  • 保持独立的测试有意义。当单个测试失败时,必须相当直接地找出它正在测试的确切内容。

我倾向于忽略测试代码中的琐碎重复,只要每个测试方法都短于大约 20 行。我喜欢设置-运行-验证的节奏在测试方法中很明显。

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

当测试方法变得冗长且重复时,我有时会发现定义采用一些参数的填空测试模板很有用。然后实际的测试方法被简化为对具有适当参数的模板方法的调用。

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

于 2008-09-24T21:17:11.980 回答
8

我同意。权衡是存在的,但在不同的地方是不同的。

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

于 2008-09-24T20:30:07.977 回答
8

您可以使用几种不同风格的测试实用方法来减少重复。

与生产代码相比,我更能容忍测试代码中的重复,但有时我对此感到沮丧。当你改变一个类的设计并且你必须返回并调整 10 种不同的测试方法,它们都执行相同的设置步骤时,这是令人沮丧的。

于 2008-09-24T21:17:47.313 回答
7

Jay Fields 创造了“DSL 应该是 DAMP,而不是 DRY”的短语,其中DAMP表示描述性和有意义的短语。我认为这同样适用于测试。显然,过多的重复是不好的。但不惜一切代价消除重复甚至更糟。测试应该作为意图揭示规范。例如,如果您从几个不同的角度指定相同的特征,那么会出现一定数量的重复。

于 2008-09-28T03:48:28.853 回答
3

“重构它们以使它们更干燥——每个测试的意图不再清晰”

听起来您在进行重构时遇到了麻烦。我只是在猜测,但如果结果不太清楚,这是否意味着您还有更多工作要做,以便您拥有相当清晰的相当优雅的测试?

这就是为什么测试是 UnitTest 的子类的原因——因此您可以设计出正确、易于验证和清晰的良好测试套件。

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

无论你使用什么语言——Python、Java、C#——你都拥有全部的力量——所以好好使用那种语言。您可以获得清晰且不太冗余的漂亮测试代码。没有取舍。

于 2008-09-25T00:03:29.163 回答
3

我喜欢 rspec 因为这个:

它有两件事可以帮助 -

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

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

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

于 2008-09-25T00:11:02.717 回答
3

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

In my experience, however, I find that well-factored tests are easier to read and understand. If there's 5 tests that each look the same except for one variable that's changed and the assertion at the end, it can be very difficult to find what that single differing item is. Similarly, if it is factored so that only the variable that's changing is visible and the assertion, then it's easy to figure out what the test is doing immediately.

Finding the right level of abstraction when testing can be difficult and I feel it is worth doing.

于 2015-05-02T22:16:56.767 回答
2

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

于 2008-09-24T20:26:47.483 回答
2

理想情况下,单元测试在编写后不应该有太大变化,所以我倾向于可读性。

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

话虽如此,我确实倾向于尝试并重用我一遍又一遍地使用的某些代码,例如在一组测试中完全相同的设置代码。

于 2008-09-24T20:29:04.843 回答