【问题标题】:Is 100% code coverage a really good thing when doing unit tests? [closed]在进行单元测试时,100% 的代码覆盖率真的是一件好事吗? [关闭]
【发布时间】:2011-03-08 14:47:51
【问题描述】:

我一直都知道,通过单元测试实现最大的代码覆盖率是。我还听到微软等大公司的开发人员说,他们编写的测试代码行数比可执行代码本身还要多。

现在,真的很棒吗?这似乎不是有时完全浪费时间,这只会使维护变得更加困难?

例如,假设我有一个方法DisplayBooks(),它从数据库中填充书籍列表。产品需求告诉如果店里有一百多本书,只能展示一百本

所以,对于 TDD,

  1. 我将首先进行单元测试BooksLimit(),它将在数据库中保存两百本书,调用DisplayBooks(),然后执行Assert.AreEqual(100, DisplayedBooks.Count)
  2. 那我会测试是否失败,
  3. 然后我将更改DisplayBooks(),将结果限制设置为 100,然后
  4. 最后我会重新运行测试看看是否成功。

好吧,直接进入第三步不是更容易,而且根本不做BooksLimit()的单元测试吗?当需求从 100 本书限制到 200 本书限制时,只更改一个字符,而不是更改测试,运行测试以检查它是否失败,更改代码并再次运行测试以检查它是否成功,这不是更敏捷吗?

注意:假设代码已完整记录。否则,有些人可能会说,他们是对的,进行完整的单元测试将有助于理解缺乏文档的代码。事实上,有一个BooksLimit() 单元测试将非常清楚地表明有一个最大数量的书籍可以显示,并且这个最大数量是 100。进入非单元测试代码会更加困难,因为这样可以通过for (int bookIndex = 0; bookIndex < 100; ...foreach ... if (count >= 100) break; 实现限制。

【问题讨论】:

  • 代码覆盖率参数有时会产生误导。除了“是否调用了一行代码”(我猜它定义了代码覆盖率),您还应该看到“它被调用了多少次”。这将允许您判断您的单元测试是否正在验证所有可能值的代码。
  • 我认为只需将函数命名为DisplayBooksShouldReturn100Books() 而不是BooksLimit() 就会给它一个明确的存在理由。
  • @Martin - 您的命名约定在什么时候过于具体。如果书籍限制更改为 120 或用户可配置怎么办。您不仅需要更改实际的书籍限制值(例如幻数),而且还需要更改方法名称。分界线在哪里?
  • @Gutz:没有具体的分界线,使用常识。通过为测试函数提供更明确的名称,它们可以更好地传达意图。 Imo 他们应该读起来像句子,即“DisplayBooks() 应该返回 100 本书”。
  • 100% 代码覆盖率本身并不是一个足够好的指标:pjohnson.info/working-code/2013/10/24/…

标签: unit-testing documentation tdd code-maintainability


【解决方案1】:

对不起我的英语。

100% 的代码覆盖率是一种管理生理障碍,可以人为地给利益相关者留下深刻印象。我们进行测试是因为存在一些可能导致缺陷的复杂代码。所以我们需要确保复杂的代码有一个测试用例,它的测试和缺陷是在它的生活之前得到修复。

我们的目标应该是测试复杂的东西,而不仅仅是所有东西。现在这个复合体需要用一些度量数来表示,这些度量数可以是以太圈复杂度、代码行数、聚合、耦合等,或者它可能是上述所有事情的顶点。如果我们发现该指标更高,我们需要确保覆盖代码的那部分。下面是我的文章,其中涵盖了代码覆盖率的最佳百分比。

Is 100% code coverage really needed ?

【讨论】:

    【解决方案2】:

    第一个 100% 很难获得,尤其是在大型项目中!即使你在一个代码块被覆盖时这样做,也并不意味着它正在做它应该做的事情,除非你的测试断言所有可能的输入和输出(这几乎是不可能的)。

    所以我不会仅仅因为它具有 100% 的代码覆盖率而认为一个软件是好的,但代码覆盖率仍然是一件好事。

    好吧,直接进入第三步不是更简单,而且根本不做BooksLimit()单元测试吗?

    在那里进行测试让您非常有信心,如果有人更改代码并且测试失败,您会注意到新代码有问题,因此您可以避免应用程序中的任何潜在错误

    【讨论】:

      【解决方案3】:

      当客户决定将限制更改为 200 时,祝你好运找到与那个看似微不足道的测试相关的错误。特别是,当您的代码中还有其他 100 个变量,并且有其他 5 位开发人员在编写依赖于那一小段信息的代码时。

      我的观点:如果它对业务价值有价值(或者,如果您不喜欢这个名称,对项目的非常重要的核心),请对其进行测试。只有在没有可能的(或廉价的)测试方法时才丢弃,例如 UI 或用户交互,或者当您确定不编写该测试的影响很小时。对于需求模糊或快速变化的项目来说更是如此 [正如我痛苦地发现的那样]。

      对于您提供的另一个示例,建议测试边界值。因此,您可以将测试限制为仅四个值:0、介于 0 和 BooksLimit 之间的某个神奇数字、BooksLimit 以及更高的某个数字。

      而且,正如其他人所说,进行测试,但要 100% 肯定其他事情可能会失败。

      【讨论】:

        【解决方案4】:

        我同意@soru,100% 的测试覆盖率不是一个合理的目标。

        我认为没有任何工具或指标可以告诉您“正确”的覆盖范围。当我在读研究生时,我的论文顾问的工作是为“变异”代码设计测试覆盖率。他进行了一套测试,然后运行一个自动化程序以在被测源代码中出错。这个想法是,变异代码包含在现实世界中会发现的错误,因此发现损坏代码百分比最高的测试套件是赢家。

        虽然他的论文被接受了,他现在是一所主要工程学院的教授,但他也没有找到:

        1) 最佳测试覆盖率的幻数 2) 任何可以找到 100% 错误的套件。

        注意,目标是找到 100% 的错误,而不是找到 100% 的覆盖率。

        @soru 的 85% 是否正确是一个有待讨论的主题。我无法评估更好的数字是 80% 还是 90% 或其他任何值。但作为一项工作评估,我认为 85% 的人认为是正确的。

        【讨论】:

          【解决方案5】:

          代码覆盖率和好的软件之间没有明确的关系。您可以很容易地想象一段代码具有 100%(或接近)的代码覆盖率,但它仍然包含很多错误。 (这并不意味着测试不好!)

          您关于“完全不测试”方法的敏捷性的问题仅适用于短期观点(这意味着如果您计划构建较长时间的程序,这很可能不好)。从我的经验中我知道,当您的项目越来越大并且在某个阶段您需要进行重大更改时,这种简单的测试非常有用。这可能是你对自己说的时刻'花一些额外的时间来编写发现我刚刚介绍的错误的小测试是一个很好的决定!”

          我最近非常喜欢代码覆盖,但现在它(幸运地)转向了类似于 '问题覆盖' 的方法。这意味着您的测试应涵盖发现的所有问题和错误,而不仅仅是“代码行”。无需进行“代码覆盖率竞赛”

          我将数字测试中的“敏捷”一词理解为 “帮助我构建优秀软件而不浪费时间编写不必要的代码的测试数量”,而不是“100% 覆盖率”或“根本没有测试”。这是非常主观的,它基于您的经验、团队、技术和许多其他因素。

          “100% 代码覆盖率”的心理副作用是您可能认为您的代码没有错误,但事实并非如此:)

          【讨论】:

          • 我不完全同意这里。 100% 的代码覆盖率并不意味着没有错误,但是如果您的测试从未使用过代码,您要么不需要该代码(-> 将其丢弃),要么您错过了一些与这段代码。当然,可能会有一些简单的事情,例如测试未涵盖的具有默认值的函数重载,但通常,您应该尝试涵盖所有代码的所有意图。
          • 好吧,我同意你的看法:)。也许你没有明白我的意思(=我没有说清楚)。我只是想指出,仅在代码覆盖率上进行中继并强制拥有 100% 的“不惜一切代价”是一条无路可走的道路(您给出了一些有效的示例)。说应该涵盖“问题”而不是“代码行”只是对您的软件的某种逆向思考,但最终这也会导致需要进行尽可能多的测试。我的意图是指出应该明智地使用保持高代码覆盖率,而不是机械地使用。
          • 我也完全同意未涵盖的代码应被视为要删除的代码,这是本次讨论的重点!如果您有未包含在测试中的代码,您可以分析它并确定为什么未包含此代码并执行以下两件事之一:将其从您的应用程序中删除或编写一些测试。我相信在这种情况下,许多程序员只是编写测试来覆盖未发现的代码,因为“100% 覆盖率宗教”没有“倾听他们的软件”。我想警告这种做法。
          • @Dzida,感谢您的澄清。事实上,我认为可以这样说:100% 的代码覆盖率不是单元测试的目标,而是它们的副作用。对于类似“基础设施”的代码(例如忽略 UI 视图等代码),单元测试仍然应该产生接近 100% 的覆盖率,并且确实应该考虑删除未使用的代码(感谢 SCM,如果您后来发现它毕竟是必需的)。
          • 没错!这是一个伟大的:“事实上,我认为 100% 的代码覆盖率不是单元测试的目标,而是它们的副作用”。感谢您的精彩讨论:)。我将再次查看我的答案并尝试将其重写为更准确(我不是母语英语的人)。
          【解决方案6】:

          100% 的单元测试覆盖率通常是一种代码异味,表明有人在覆盖率工具中的绿色条上克服了所有的强迫症,而不是做一些更有用的事情。

          大约 85% 是最佳点,测试失败的频率更高,这并不表示存在实际或潜在问题,而不仅仅是注释标记内任何文本更改的必然结果。如果您的假设是“代码就是它的样子,并且如果它有任何不同,那就是别的东西”,那么您就没有记录任何关于代码的有用假设。这是一个注释感知校验和工具解决的问题,而不是单元测试。

          我希望有一些工具可以让您指定目标覆盖范围。然后,如果您不小心检查了它,请以黄色/橙色/红色显示内容,以促使您删除一些虚假的额外测试。

          【讨论】:

          • 不同意(尽管我不会投反对票)。 100% 的代码覆盖率是理想的,但不是必需的。根据我的经验,如果程序员花费大量时间来达到 100%,这可能意味着正在测试的代码需要重构。你所做的每一个假设都是一行代码,需要验证。仅仅调用这条线是不够的——它的效果和副作用还必须得到验证。
          • 很公平,但在我看来,100% 重复测试中源代码的每一个细微差别与没有任何测试没有区别;在这两种情况下,您都会更改某些内容,并且您不知道更改是否不好。一种情况是因为没有测试会失败,另一种情况是因为测试没有失败的余地。
          • 很公平。您指的是盲目测试——这是绝对必须避免的,并且在执行 100% 规则的项目中很常见。
          • 你的第一句话很精彩,不管是真是假。
          【解决方案7】:

          好吧,直接进入第三步不是更简单,而且根本不做BooksLimit()单元测试吗?

          是的...如果您不花时间编写测试,您将花费更少的时间编写测试。您的项目总体上可能需要更长,因为您会花费大量时间进行调试,但也许这更容易向您的经理解释?如果是这样的话……找一份新工作!测试对于提高您对软件的信心至关重要。

          当您拥有大量代码时,单元测试最有价值。使用几个类无需单元测试即可轻松调试简单的家庭作业。一旦你走出世界,并且在数百万行的代码库中工作 - 你将需要它。您根本无法单步调试您的调试器。你根本无法理解一切。您需要知道您所依赖的课程。您需要知道是否有人说“我只是要对行为做出这种改变......因为我需要它”,但他们忘记了还有 200 种其他用途取决于这种行为。单元测试有助于防止这种情况发生。

          关于使维护更加困难:没办法!我不能充分利用这一点。

          如果您是唯一参与过您的项目的人,那么是的,您可能会这么认为。但那是疯狂的谈话!尝试在没有单元测试的情况下加快 30k 行项目的速度。尝试在没有单元测试的情况下添加需要对代码进行重大更改的功能。 没有信心你没有打破其他工程师的隐含假设。对于维护者(或现有项目的新开发者)来说,单元测试是关键。我依靠单元测试来记录文档、行为、假设、告诉我什么时候破坏了某些东西(我认为这些东西不相关)。有时,一个写得不好的 API 的测试写得不好,而且可能是一场噩梦,因为测试会占用你所有的时间。最终你会想要重构这段代码并修复它,但你的用户也会为此感谢你——你的 API 将因此更容易使用。

          关于覆盖范围的说明:

          对我来说,这不是 100% 的测试覆盖率。 100% 覆盖并没有找到所有的错误,考虑一个有两个if 语句的函数:

          // Will return a number less than or equal to 3
          int Bar(bool cond1, bool cond2) {
            int b;
            if (cond1) {
              b++;
            } else {
              b+=2;
            }
          
            if (cond2) {
              b+=2;
            } else {
              b++;
            }
          }
          

          现在考虑我写一个测试来测试:

          EXPECT_EQ(3, Bar(true, true));
          EXPECT_EQ(3, Bar(false, false));
          

          这是 100% 的覆盖率。这也是一个不符合约定的函数——Bar(false, true); 失败,因为它返回 4。所以“完全覆盖”不是最终目标。

          老实说,我会跳过BooksLimit() 的测试。它返回一个常量,因此可能不值得花时间编写它们(并且应该在编写 DisplayBooks() 时对其进行测试)。当有人决定(错误地)根据货架尺寸计算该限制时,我可能会感到难过,它不再满足我们的要求。我以前被“不值得测试”烧伤。去年我写了一些代码,我对我的同事说:“这个类主要是数据,不需要测试”。它有一个方法。它有一个错误。它开始生产了。它在半夜寻呼我们。我觉得很愚蠢。所以我写了测试。然后我对哪些代码构成“不值得测试”进行了长期而艰苦的思考。没有多少。

          所以,是的,您可以跳过一些测试。 100% 的测试覆盖率很棒,但这并不意味着你的软件是完美的。这一切都归结为面对变化时的信心。

          如果我将class Aclass Bclass C 放在一起,我发现有些东西不起作用,我要花时间调试这三个吗?不。我想知道AB 已经满足了他们的合同(通过单元测试),而我在class C 中的新代码可能已损坏。所以我对它进行了单元测试。如果我不进行单元测试,我怎么知道它坏了?通过单击一些按钮并尝试新代码?这很好,但还不够。一旦你的程序扩大规模,就不可能重新运行所有手动测试来检查一切是否正常。这就是为什么进行单元测试的人通常也会自动运行他们的测试。告诉我“通过”或“失败”,不要告诉我“输出是……”。

          好的,去写更多的测试......

          【讨论】:

          • 不是所有的东西都可以用单元测试来测试。调试不仅仅是编写单元测试。试用该软件、查找 UI 错误以及人工 Beta 测试对于查找错误也是必不可少的。问题是:纠正每个用户都会看到(并为此烦恼)的图形故障或可以通过单元测试找到但在使用软件?当您可以花时间使用您的软件来了解在哪里改进它时,是否值得花更多时间编写测试而不是编码?
          • @Nico :这是一个很好的观点。我忽略了谈论其他类型的测试(主要是因为问题是针对单元测试的)。系统测试已证明(对我而言)是必不可少的。我主要编写基础设施,它提供合同的实现,因此单元测试效果很好。我没有要测试的 UI,但如果我有,测试它会很重要。
          • @Stephen:我当然不是说单元测试总是没用的。我只是说一个人不应该仅仅依靠他们说他们的软件是“无错误 (TM)”:)
          • @nico,单元测试应该测试独立的功能,它们既不是系统测试也不是集成测试,也不能取代那些。但是,通过验证代码单元的意图,它们可以确保代码的整体行为稳定,尤其是在进行更改/增强时,因此也减少了更复杂的后续成本(在往返时间和工作量方面),因此更“昂贵”的复杂测试。
          • @MainMa :完全正确。文档的准确性受到侵蚀,人们只是不阅读它——谁能责怪他们呢? “真相”在代码中!单元测试是检查这些需求的一种方式。最近,作为“新人”,我的很多问题都是“我将行为从 X 更改为 Y 并且没有单元测试失败。行为 X 重要吗?有人会关心吗?”。如果答案是“是”,则说明测试不足,因此我们添加一些。
          【解决方案8】:

          在查看一个孤立的问题时,您是完全正确的。但是单元测试是关于覆盖你对某段代码的所有意图。

          基本上,单元测试会表达您的意图。随着越来越多的意图,要测试的代码的行为总是可以根据迄今为止所做的所有意图进行检查。每当进行更改时,您都可以证明不存在破坏现有意图的副作用。新发现的错误只不过是代码不包含的(隐式)意图,因此您将意图表述为新测试(最初失败)并修复它。

          对于一次性代码,单元测试确实不值得努力,因为预计不会发生重大变化。但是,对于要维护或用作其他代码组件的任何代码块,保证所有意图都适用于任何新版本是非常值得的(就手动尝试检查副作用的工作量而言) .

          单元测试实际上为您节省时间和金钱的临界点取决于代码的复杂性,但总是存在一个临界点,通常只需要经过几次更改迭代即可达到。此外,最后但并非最不重要的一点是,它允许您更快地发布修复和更改,而不会影响产品质量。

          【讨论】:

          • 也许也值得一读:cwd.dhemery.com/tag/tdd
          • @nico,感谢您的链接。但是,在我看来,它完全忽略了单元测试的重点。您无法对复杂的应用程序甚至 UI 进行单元测试。但是你不应该使用单元测试来做到这一点。它们应该真的没有任何外部依赖或不确定的部分,或者它们不是概念上的单元测试。也就是说,我同意文章作者的观点,即您应该在防御性问题上编写高质量的代码,并且您不能只编写一些单元测试并认为应用程序已经过测试。但是单元测试非常适合保持意图在更改时有效。
          • 当然。我猜那篇文章的作者有点太“粗鲁”了(我猜是故意的)。他说你不应该仅仅依赖他们,我认为我们同意。
          猜你喜欢
          • 2023-03-21
          • 2022-07-04
          • 2023-03-24
          • 1970-01-01
          • 2012-01-18
          • 1970-01-01
          • 2016-05-04
          • 2010-10-14
          相关资源
          最近更新 更多