【问题标题】:What should a "unit" be when unit testing?单元测试时“单元”应该是什么?
【发布时间】:2010-11-07 04:25:25
【问题描述】:

今天在 Proggit 上,我正在阅读题为“Why Unit Testing Is A Waste of Time”的提交的评论线程。

我并不真正关心文章的前提,而是关注comment 的相关内容:

问题的根源在于商业软件中的大多数代码“单元” 项目是微不足道的。

改变单位的大小,直到 不再是小事?谁他妈 将代码单元定义为单个 函数还是方法!?

嗯,我一起工作的一些人 想将一个单元定义为单个 职能。这完全是愚蠢的。 我最喜欢的“单位”定义是: 最小的一段代码 可以进行有用的测试。

我们是否花费了太多时间来模拟一些对象并测试一段微不足道的代码,而没有真正添加任何有价值的东西?

在进行单元测试时,“单元”应该是什么?功能级别测试是否过于细化?

【问题讨论】:

标签: unit-testing tdd


【解决方案1】:

引用Wikipedia可能看起来微不足道,但我认为在这种情况下它非常简洁准确:

单元是应用程序中最小的可测试部分。

这似乎与您问题中的评论一致,即单元是“可以有效测试的最小代码片段”。换句话说,尽可能使单元尽可能小这样它对开发人员/测试人员本身仍然有意义

您通常希望单独测试项目的各个部分,然后测试它们如何组合交互。拥有不同层级(级别)的单元测试通常是明智之举,因为它有助于确保您的代码在各个级别(从单个功能到整个独立任务)上都能正常工作。我个人不认为测试个别功能是错误的,甚至是无益的,只要他们自己做一些有用的事情,通常情况就是这样。

老实说,“单元测试”中的“单元”没有明确或严格的定义,这正是使用模糊术语“单元”的原因!了解需要测试的内容和测试级别是经验问题,而且通常只是反复试验。这听起来可能有点让人不满意,但我相信这是一个可以遵循的合理规则。

【讨论】:

  • @Noldorin:
【解决方案2】:

“单位”应该是一个根据您的定义的原子单位。也就是说,准确地定义单元测试的“单元”是相当主观的。它可能很大程度上取决于您的架构是什么,以及您的应用程序如何选择分解功能单元。我的意思是没有明确定义的“单位”,除了你定义的。 “单元”可以是具有单一副作用的单个方法,也可以是一组相关的方法,它们共同定义了一个单一的、连贯的功能集。

理性在这里很重要;完全可以为您拥有的每个类的每个访问器编写单元测试;但是,这显然是矫枉过正。同样,将整个应用程序定义为一个单元并期望有一个测试来测试它是可能的,但很愚蠢。

通常,您需要一个用于测试的“单元”来描述一个明确定义、明确不同的功能块。在查看应用程序的每个级别,您都应该能够看到清晰的功能“原子”(如果您的设计很好的话);这些可以像低级视图的“从数据库中检索记录”一样简单,也可以像高级视图的“创建、编辑和删除用户”一样复杂。它们也不是相互排斥的。您可以轻松地将两者都作为单元测试。

这里要注意的是,您要测试的单元是有意义的单元。您希望单元测试来验证和验证功能;所以单元测试测试就是你定义的功能。什么是功能单位?这取决于您的个人情况来定义。

【讨论】:

    【解决方案3】:

    我一直在功能级别进行测试,效果很好。对我来说最重要的是单元测试测试了两段代码之间的契约。单元测试只是充当调用者,并确保被测试的代码(无论有多大——单个函数或需要 30 分钟测试的巨大的嵌套库)以可预测的方式返回结果。

    单元测试的重点是确保兼容性(通过不破坏交互)并确保可预测的结果。在任何可以交换信息的地方进行测试都有助于稳定您的应用程序。

    更新:每当客户或用户报告破坏我的应用程序交互的边缘情况时,我也总是添加单元测试。修复它对我来说还不够 - 添加一个单元测试以确保修复能够防止回归,并有助于保持稳定。

    【讨论】:

    • 如果你在调试一个容器,单独测试 put、fetch 和 reduce 有什么意义?一个严肃的测试应该将所有这些都包含在一个测试函数中。
    • 我不确定我是否理解您的要求,但像往常一样,这取决于 - 如果使用一些测试数据的单个“此容器按我预期的方式运行”是可以的,那么将它们组合成一次测试。如果您希望看到功能被分解,那么将其分解为多个测试。还值得一提的是,单元测试(至少对我而言)是一个测试点——如果我想强制应用程序行为不端,我可以稍微修改我的测试,而不必在更大的上下文中破坏对象应用程序,我可以根据我的测试测试单个对象或对象的功能。
    • @appaka:感觉是,如果您的put 实现被破坏,那么只有您的put 测试会失败。然而,如果您只进行了一次测试,您所知道的只是“在putfetchreduce 的任一单独操作中,或者可能在它们一起使用的某种组合中出现了问题。”从该信息到“put 已损坏”需要大量额外的工作,并且会使您的单元测试的用处大大降低。
    【解决方案4】:

    我想说一个单元是可以与伪代码中的其他步骤分开的最小的有意义的工作部分。

    例如,如果您尝试执行一系列步骤来处理某些数据,例如

    1. 扫描数据集的最小值、最大值、中值、平均值
    2. 生成直方图
    3. 生成重映射函数
    4. 重新映射数据
    5. 输出数据

    每个步骤都是一个“单元”,整个系列本身也是一个“单元”(测试每个步骤之间的凝聚力是否有效)。这些步骤中的任何一个都可以是四个函数(对于第一步,如果程序员正在运行四个循环),或者一个函数,或者其他什么,但最终,已知的输入应该给出已知的输出。

    【讨论】:

      【解决方案5】:

      如果您考虑墨菲定律,没有什么是微不足道的。

      开玩笑,假设一个 OO 环境,我以类为单位进行单元测试,因为各种方法经常修改内部状态,我想确保各种方法之间的状态是一致的。

      不幸的是,检查一致性的唯一方法通常是调用类的各种方法来查看它们是否失败。

      【讨论】:

        【解决方案6】:

        一般来说,您制作的单元越小,您的单元测试就越有用和有效。单元测试的重点是能够将任何新引入的错误本地化到代码的一小部分。

        【讨论】:

        • 使用您描述的方法:如何在不破坏所有测试的情况下重构代码?
        • @Nathan 在一个完美的世界中,黄金标准是,您的接口不应更改,但该接口背后的实现/业务逻辑可能会更改。如果您的单元测试与实现逻辑如此耦合,那么它一开始就是一个糟糕的单元测试,或者您的测试数据和您的实现逻辑数据之间存在某种重复。
        【解决方案7】:

        对我来说,“单元”是一个班级的任何重要行为,在接下来的 8 到 12 个月内,我不会记得了。

        如果有编写良好的单元测试(想想BDD),我就不必这样做了,因为我的测试会用简单的英语向我描述并验证代码。

        【讨论】:

          【解决方案8】:

          我怀疑“单元测试”是一个误用的术语。如今,该术语不仅用于指代测试一小段代码,还用于任何自动化测试。

          我倾向于在编写代码之前编写测试,所以当我第一次编写它时,我无法告诉你它是否会导致创建一些新行、一个新方法或一个新类。

          总之:mu

          【讨论】:

            【解决方案9】:

            这个问题很老了,没有准确的答案,但我认为“任何有意义的东西”或“可以进行有用测试的最小部分”可以稍微改进一下。

            有一篇很棒的文章公然命名为“Unit tests aren't tests”。标题是一个点击诱饵,它本身是一个很好的阅读,但我会在这里只强调相关的点。

            物理学中有一个叫做“emergence”的想法,其中简单 与简单规则交互的系统会产生复杂系统 按照有效的不同规则行事。例如,原子是相对容易理解的、独立的模型。把它们塞进一个盒子里,突然间你就拥有了整个固态物理学领域。 ...代码展品出现 也。足够多的交互单元和你的程序要复杂得多 比其部分的总和。即使每个单元都表现良好并且工作 根据其单元测试,大部分复杂性在于 整合。

            基本上尝试围绕“可组合单元”组织单元测试,即组合在一起时不太容易受到出现影响的东西 - 但测试仍然应该足够简单和快速以被称为“单元测试”。出现的影响无法完全消除,并且无论如何都会在单元之间“隐藏”一些复杂性 - 但是它应该是可以通过集成/系统测试处理的相对少量的复杂性(这反映在测试金字塔结构中)。

            不幸的是,无法通过工具来衡量高可组合性和出现的效果,我只能在脑海中给出几个想法:

            • 大量使用模拟和存根是一种气味。无需编写任何有用的测试即可轻松使用模拟并报告几乎 100% 的覆盖率。
            • 纯不可变单元更易于组合。在纯函数式编程中,通常会在非常高的级别上进行“单元测试”。
            • 对于设计不佳的系统,没有“正确”级别的单元测试。无论您尝试测试什么单元,它都会出现。这就是为什么在重构一些意大利面条式编码的遗留系统的同时投资集成测试是一个常见的建议。

            【讨论】:

            • 我不同意这种分析。主要是因为模拟和存根并不意味着测试不好。存根的目的是隔离被测试函数的功能。存根需要向被测函数返回有效值和无效值,以保证当所有东西确实集成在一起时,被测函数在返回或模拟值的情况下将按预期运行。存根不会破坏集成。相反,它使执行短路,并允许我们人为地提供值,而无需执行其他地方存在的代码。
            • 答案并没有说模拟和存根意味着测试不好。它说“大量使用模拟和存根是一种气味”。
            【解决方案10】:

            改变单位的大小,直到 不再是小事?谁他妈 将代码单元定义为单个 函数还是方法!?

            如果测试定义为方法的“单元”太难,则很可能该方法太大或太复杂而无法编写单元测试。

            我遵循 rwmnau 建议的类似方法,并在方法级别进行测试。除了为每个方法创建一个测试之外,我还为每个方法中的不同代码路径创建了额外的测试。有时,强调所有代码路径可能会变得复杂。我的挑战是尽量避免编写方法的类型,除非在性能和代码复杂度方面没有更好的解决方案。

            我还使用模拟来测试组件之间的合同。

            【讨论】:

              【解决方案11】:

              就像单元测试中的简单词 unit 是您的应用程序的单元行为/功能 或您要测试的系统。您的测试用例应该测试单元行为而不是方法。仅仅针对小测试不会产生高质量的测试。这应该是有道理的。

              这是Vladimir Khorikov在他的书中的例子-

              测试应该讲述你的代码有助于解决的问题的故事,而且这个故事对于非程序员来说应该具有凝聚力和意义。

              例如,这是一个有凝聚力的故事的例子:

                  When I call my dog, he comes right to me.
              

              现在将其与以下内容进行比较:

                  When I call my dog, he moves his front left leg first, then the front right
                  leg, his head turns, the tail start wagging...
              

              第二个故事意义不大。所有这些运动的目的是什么?狗来找我了吗?还是他在逃跑?你说不出来。当您针对单个类别(狗的腿、头和尾巴)而不是实际行为(狗走向他的主人)时,这就是您的测试开始的样子

              让我们再举一个实际的代码示例,一个经典的 ATM 机示例。有一个ATM类,它包括以下方法

              • 撤回();

              • getBalance();

              • 存款();

              您将为 deposit() 编写类似这样的测试用例 - -makeASingleDeposite

              • makeAMultipleDeposit

              对于withdraw() 是这样的—— -makeSingleWithDraw -makeMultipleWithDraw

              您必须测试 ATM 类 (SUT)、withdraw() 和 deposit() 的行为。不需要编写单独的测试用例来测试 getBalance()。至于在测试用例中验证提款和充值行为,可以通过调用getBalance()方法来验证余额。

              从 OOP 的角度来看,当您是测试类时,您正在测试类的聚合行为,而不是其单个方法。

              如果类不包含任何状态,例如服务或 DAO 等,并且您使用事务脚本模式实现了业务逻辑。然后你的每一个方法都像过程一样,你可以将方法视为单元。编写一个考虑该方法提供的功能的测试用例。

              在 FP 的情况下也是如此,您的方法/方法或功能就是您的单位。

              【讨论】:

                猜你喜欢
                • 2017-05-17
                • 2011-01-19
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2021-11-01
                • 1970-01-01
                相关资源
                最近更新 更多