【问题标题】:Is duplicated code more tolerable in unit tests?重复代码在单元测试中是否更容易容忍?
【发布时间】:2008-09-24 20:22:10
【问题描述】:

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

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

【问题讨论】:

    标签: unit-testing dry code-duplication


    【解决方案1】:

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

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

    【讨论】:

    • xUnit 和其他在断言调用中包含“消息”参数。加入有意义的短语以让开发人员快速找到失败的测试结果是个好主意。
    • @seand 您可以尝试解释您的断言正在检查什么,但是当它失败并且包含一些模糊的代码时,开发人员无论如何都需要去解开它。 IMO 更重要的是让代码在那里进行自我描述。
    • 因为报告的可读性比测试更重要,特别是在进行集成或端到端测试时,场景可能足够复杂以避免导航一点点,找到故障是可以的,但又一次对我来说,报告中的失败应该足以很好地解释问题。
    • 可读性在测试和非测试代码中同样重要。重构不应该损害可读性——它应该增强可读性。
    • 这里提出的一个很好的理由来自 DRY 和单元测试背后的基本假设:重复自己是不好的,因为它增加了改变行为所需的工作量。另一方面,如果您必须经常更改单元测试,则很有可能您的单元测试过于严格——他们测试的是how 而不是what。理想的单元测试在布局后不会改变。这意味着将 DRY 应用于单元测试不会像应用于应用程序代码那样带来更多的利润。 DRY 是对变化的反应。单元测试不应该。
    【解决方案2】:

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

    如果重复是在夹具设置中,请考虑更多地使用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 测试的明确性很重要。我发现如果许多测试看起来基本相同(例如,四分之三的行相同或几乎相同),如果不仔细阅读和比较它们,就很难发现和识别显着差异。所以我发现重构以消除重复有助于可读性,因为每个测试方法的每一行都与测试的目的直接相关。这比直接相关的行和只是样板的行的随机组合对读者更有帮助。

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

    【讨论】:

    • “重复代码是单元测试代码中的一种气味,就像其他代码中一样。”不会。“如果你在测试中有重复的代码,那么重构实现代码就会变得更加困难,因为你要更新的测试数量不成比例。”发生这种情况是因为您正在测试私有 API 而不是公共 API。
    • 但是为了防止单元测试中的重复代码,您通常需要引入新的逻辑。我认为单元测试不应该包含逻辑,因为那样你就需要单元测试的单元测试。
    • @user11617 请定义“私有 API”和“公共 API”。在我的理解中,公共 Api 是对外部世界/第 3 方消费者可见并通过 SemVer 或类似方式明确版本化的 Api,其他任何内容都是私有的。有了这个定义,几乎所有的单元测试都在测试“私有 API”,因此对代码重复更加敏感,我认为这是真的。
    • @KolA "Public" 并不意味着第 3 方消费者 - 这不是 Web API。类的公共 API 指的是客户端代码使用的方法(通常不会/不应该改变那么多) - 通常是“公共”方法。私有 API 是指内部使用的逻辑和方法。这些不应该从课堂之外访问。这就是为什么使用访问修饰符或所用语言中的约定将逻辑正确封装在类中很重要的原因之一。
    • 奇怪的是,我同意 spiv 和 Kristopher Johnson 的回答。我会这样解释:你总是希望避免在测试中重复实现细节,但绝不要以隐藏上下文为代价。删除重复的糟糕选择是隐藏上下文的继承链和类型组合。删除重复的好选择是工厂、构建器和对象母亲。
    【解决方案3】:

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

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

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

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

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

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

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

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

    【讨论】:

    • +1 表示“我喜欢设置-运行-验证的节奏在测试方法中很明显。”正是我要说的。因此,在提取执行两个或多个 setup-run-verify 阶段的通用方法之前,我会三思而后行
    【解决方案4】:

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

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

    【讨论】:

    • 我认为这是个好主意。如果您有很多重复,请查看是否可以重构以创建一个通用的“测试夹具”,在该夹具下可以运行许多测试。这将消除重复的设置/拆卸代码。
    【解决方案5】:

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

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

    【讨论】:

      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

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

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

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

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

        无论您使用何种语言(Python、Java、C#),您都拥有强大的能力,因此请好好使用该语言。您可以获得清晰且不太冗余的漂亮测试代码。没有取舍。

        【讨论】:

          【解决方案8】:

          因为这个,我喜欢 rspec:

          它有两件事可以帮助 -

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

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

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

          【讨论】:

            【解决方案9】:

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

            然而,根据我的经验,我发现精心设计的测试更容易阅读和理解。如果有 5 个测试,除了一个变量发生变化和最后的断言之外,每个测试看起来都一样,那么很难找到那个不同的项目是什么。类似地,如果考虑到只有变化的变量和断言是可见的,那么很容易立即弄清楚测试在做什么。

            在测试可能很困难时找到正确的抽象级别,我认为这是值得的。

            【讨论】:

              【解决方案10】:

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

              【讨论】:

                【解决方案11】:

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

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

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

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2023-03-17
                  • 2012-05-16
                  • 1970-01-01
                  • 1970-01-01
                  • 2010-12-03
                  • 2015-05-24
                  • 2012-12-06
                  • 2023-02-16
                  相关资源
                  最近更新 更多