当测试失败时,代码库与测试断言的“正常”比率是什么?

时间:2010-02-16 19:56:05

标签: unit-testing testing tdd

我目前正在开发一个项目,其中包含一些相当重要的业务规则,当我们编写解决方案时,问题空间被“发现”(非常典型的混乱项目管理类型)。我们有良好的测试覆盖率和依靠它们相当多,以确保我们的重大变化不会打击任何东西。这种情况是单元测试狂热者强调的一个主要例子,即如果你不使用单元测试,那么测试帮助软件可以立即轻松修改,缺陷更少,更快完成。我不禁思考如何在没有测试套件的情况下应对。

我的问题是,虽然我当然相信单元测试的价值(这个项目实际上是TDD,但它与问题没有密切关系),我和其他人一样,想知道经典的单元测试问题。更多的代码需要整理和维护(即测试本身)。再次。 在我的脑海中毫无疑问,这个特别的项目在单元测试方面要好得多,没有它,我也关注测试的长期可维护性。

我根据其他人的建议使用了一些技巧来帮助解决这个问题。一般来说,

  1. 我们创建的测试列表位于“从属”与“独立”存储桶中。独立测试不需要任何不受源代码控制的东西。因此,对我们的数据访问层的任何调用都是模拟或从xml文件而不是真实数据库获取数据。名称建议的依赖测试取决于配置文件或数据库或网络事物,在运行测试时可能无法正确配置/可用。将测试分成这样的组对于允许我们编写依赖的“丢弃”测试以进行早期开发和独立的任务关键测试(可以依赖并重新测试腐烂)非常有用。它还使CI服务器易于管理,因为它不必设置和维护数据库连接等。
  2. 我们定位不同级别的代码。例如,我们有测试命中'main'并测试命中'main'会调用的所有方法。这使我们能够定位系统的细节和总体目标。如果“主要”测试中断,它们很难调试,但它们通常不是唯一中断的(详细测试也会中断)。如果它们中断,则更容易遵循详细测试并调试它们,但它们不足以知道重构是否会导致系统崩溃(这就是“主要”测试的用途)。
  3. “主要”测试至关重要,因为重构没有给代码库带来麻烦。因此,对于使用映射到用例的不同args调用的单个方法,“主要”测试就像许多测试一样。它基本上是我们在最高级别的代码的入口点,因此可以说不是真正的“单元”测试。但是,我发现我真的需要更高级别的测试才能感觉到重构没有炸毁代码库。较低级别的测试(那些真正“工作单位”的测试是不够的。
  4. 这一切都是为了解决这个问题。随着项目向前发展,我发现我需要对代码库实施更改(有时非常重要,有时是微不足道的),我发现当更改导致测试失败时,测试失败与实际回归业务逻辑之间存在比率失败和单元测试无效。换句话说,有时测试失败是因为实际代码库中存在回归错误,有时这是因为单元测试断言不再有效,而且是需要更改的断言。似乎粗略地说,当测试失败时,它对于这个特定的项目来说差不多(50%)。

    是否有人在他们的项目中跟踪过这个比例,如果有,那么你对这个比例学到了什么(如果有的话)?我不确定它是否表明任何东西,但我注意到大约一半的测试失败导致我调整测试断言,而不是实际修复真实代码库中的回归错误。无论什么时候发生这种情况,都会让我觉得我只是浪费了我一天中的x小时。我想知道我的测试方法能否以某种方式提高效率。解决测试断言失败通常比实际的回归错误需要更长的时间,而这些错误既有反作用又令人沮丧。

    编辑 请注意,这个问题是关于探索此比率的含义以及您对此比率的体验。它什么时候“臭”?

4 个答案:

答案 0 :(得分:4)

“测试失败导致我调整测试断言而不是实际修复真实代码库中的回归错误。”

正确。您的要求已更改。您的测试断言必须更改。

“这让我觉得我只是浪费了我一天中的x小时”

为什么呢?你还怎么跟踪需求变化?

“解决测试断言失败的时间通常比实际的回归错误”

不开玩笑。当您的需求处于不稳定状态时,需要花费大量时间和精力将需求更改映射到测试结果更改。

“这是......违宪的”。取决于你的直觉。我的直觉(经过18个月的TDD)是需求变化导致设计变更,许多复杂的测试变化以反映设计变化。

有时,很少(或没有)代码更改。

如果你的代码真的很好,它的变化不大。当你花在测试上的时间多于代码时,这意味着你编写了很好的代码。

快乐回家。

当您花费更多时间尝试获取代码以通过一组永不改变的测试时,会出现代码气味。想想这意味着什么。你编写了测试,但是你无法通过代码。那太可怕了。

如果你花一个小时写测试,4个小时试图让代码通过测试,你要么得到一个非常复杂的算法(并且应该把它分解成更多可测试的部分),或者你是一个糟糕的应用程序程序员。

如果你花1小时写测试和1小时获得代码通过测试,这很好。

如果由于需求变更而花费2小时修复测试,并且1小时获得代码以通过修订后的测试,则意味着您的代码对变更的抵抗力不强。

如果由于需求变更而花费2个小时修复测试,并且通过这些测试需要1/2小时调整代码,那么您已经编写了一些非常好的代码。

答案 1 :(得分:3)

  

我注意到大约一半的测试失败导致我调整测试断言而不是实际修复真实代码库中的回归错误。

当测试失败时,有三个选项:

  1. 实施已经破解,应该修复,
  2. 测试被破坏,应该修复,或
  3. 不再需要测试(因为需求已更改),应删除。
  4. 正确识别这三个选项中的哪一个很重要。我编写测试的方式,我在测试的名称中记录了测试指定/测试的行为,这样当测试失败时,我会很容易地找到为什么测试最初编写。我在这里写了更多关于它的信息:http://blog.orfjackal.net/2010/02/three-styles-of-naming-tests.html

    在你的情况下,

    • 如果由于需求更改而需要更改测试,并且一次只需要修改几个,那么一切正常(测试很好isolated,这样每个行为只由一个测试指定。)
    • 如果由于需求更改而需要更改测试,并且许多测试需要一次更改,那么您需要进行大量测试test smell (测试隔离良好)。测试可能正在测试more than one interesting behaviour。解决方案是编写更集中的测试和更好的解耦代码。
    • 如果需要在重构时更改,那么测试气味与测试实施细节过于紧密相关。尝试编写以系统行为为中心的测试,而不是其实现。我之前链接的The article应该会给你一些想法。

    (一个有趣的观点是,如果你发现自己主要是重写类,而不是更改它们,当需求发生变化时,那么它可以表明代码遵循良好{{3和其他设计原则。)

答案 2 :(得分:1)

我绝对是第二个@ S.Lott的回答。我只想指出,当规则建立在成堆的死树上时会发生的情况是,当需求发生变化时,死树(或文字处理器文件)不会像测试那样对你大喊大叫,所以一切都在运行很好,除了你有这堆死树,每个人都看,并说“文件是虚构的。”

话虽这么说,有些情况下测试的编写不好或有用,可能应该删除。我发现特别是在TDD中,测试改变了设计,并且实际上是增量的,现在设计和功能更进一步,其中一些原始测试实际上不再相关。

如果你认为将一堆测试修复为“浪费了我一天中的x小时”,那么当你进行第二次测试时,你不会考虑第一次测试后的第一次测试。这将使变革成本更高。这可能是正确的决定,但是看一个测试并说它被后续事件克服并且只是放弃它就没有错,只是不要把它作为一种便宜的出路。

答案 3 :(得分:0)

S上。洛特几乎都说了这一切。我认为测试断言改变(T)与回归修正(R)的比例唯一可以告诉你的是你的需求的波动性(这会使T更高)与应用程序代码通过时的成功程度的组合。测试(这会影响R的值)。这两个因素可能会根据您的要求和开发过程的质量而独立变化。