TDD是否可以成为过度数据验证的有效替代?

3
考虑以下两种数据验证方案: 无处不验 确保每个接受一个或多个参数的方法都对其进行检查,以确保它们在语法上是有效的。 优点
  • 非常细致的检查粒度。
  • 如果编写的代码是某种库的一部分,我们可以确保限制在使用它的开发人员未能提供有效数据时可能造成的损害。
缺点
  • 总是执行大多数情况下不需要的检查成本高。
  • 仍有可能偶尔忘记添加检查。
  • 编写更多代码,因此需要维护。
利用TDD的好处 仅在外部世界输入数据时验证数据。为了确保内部数据始终具有语法正确性,请创建测试,以检查每个返回值的方法。以确保如果输入有效数据,则输出有效数据。
优点和缺点与前一方法相比基本相反。
目前我正在使用第一种方法,但由于我正在采用测试驱动开发,所以我想也许我可以使用第二种方法。
优点很明显,但我想知道它是否像第一种方法那样安全。
5个回答

2
听起来第一种方法是基于合同的,而其中的一个方面是您还需要验证从任何公共接口返回的内容是否符合合同。
但是,我认为这两种方法都是有效的,但非常不同。
TDD仅部分处理公共接口,因为它应该检查每个输入是否被正确验证,但不幸的是,除非您在单独的函数中有所有的验证,否则要充分测试,就会变得非常难以确保这个带有3或4个参数的函数被正确地测试了其有效性。无论采用哪种方法,你必须写很多测试。
如果您使用了一个库,那么在可以直接从外部调用的每个函数中,您都需要检查每个输入是否有效,并且无效输入是否按照合同进行处理,可以返回null或抛出异常。但是,必须与文档一致。
一旦您已经验证了它,那么就没有理由强制对私有函数进行验证,因为这些只能从库内部调用,而您应该验证您只处理有效数据。
无论如何,都需要大量的测试,不幸的是。所有这些测试所做的就是确保您没有任何意外问题,但是通常会帮助证明编写和维护它们的成本是合理的。
至于您的问题,如果您的测试编写得非常好,并确保所有有效性检查都已完全完成,则应该是安全的,但风险在于如果您认为它很安全而且测试编写得不好,那么它实际上会比没有测试更糟糕,因为这里有一个假设,即你的测试编写得很好。
在了解您的测试编写得很好之前,我建议您使用两种方法,然后只使用TDD。

同意。通常我会担心我的公共接口,并假定内部方法的参数是有效的(更多是为了保持方法短小精悍)。我想这些方法可能会在重构/复用中冒泡到公共接口,但如果您始终测试您的公共接口... - TrueWill
实际上,我会使用AspectJ来测试我的Java公共函数,以确保输入参数和返回数据与文档一致。 - James Black

1

我的观点是,在第一种情况下,你的两个缺点超过了其他一切:

  • 总是执行大多数时候不需要的检查是很昂贵的。
  • 编写更多的代码,因此需要维护。

此外,从技术上讲,TDD与这个问题无关,因为它不是一种测试技术。稍后再说...

为了减轻缺点,我强烈建议(就像你所说的那样)将代码分成外部内部:外部是所有验证发生的地方。希望这只是内部的一个薄包装,以防止GIGO。一旦进入内部,数据就不需要再次进行验证。

关于TDD,我强烈主张(正如您现在所做的)采用它来开发您的代码,这样可以留下一系列测试作为回归测试套件的额外优势。现在您将自然而然地开发您的外部代码以执行强大的验证,并承诺轻松添加任何您可能最初忘记的检查。您的内部代码可以被开发者假定它只能处理有效数据,但TDD仍然可以让您有信心它将按照规格运行。
我要说的是,无论我是否正在使用TDD,我都会采用我所描述的第二种方法(但TDD始终是我的首选)。

0
优点很明显,但我还是想知道它是否像第一种方法那样安全。

这完全取决于您的测试质量。

如果满足以下两个条件,则此方法可能同样安全:

  • 系统中每个公开暴露的添加数据的方式都得到了完全验证
  • 每个翻译数据的内部方法都经过了完全和充分的测试

然而,我怀疑这会更容易或需要更少的代码。检查每个公共入口点所需的代码量将非常类似于验证每个方法所需的代码量。由于它们必须检查可能在内部检查的事物,因此入口点需要更多的检查。


0
对于第二种方法,您需要两组良好的测试。您不仅必须检查:

确保如果输入有效数据,则输出有效数据。

您还必须检查:如果输入无效数据,则会抛出异常。我想您仍然需要验证数据并在有无效数据时退出。如果您不想在生产应用程序中遇到烦人的ArgumentNullException或其他神秘错误,那么这确实是唯一的方法。但是,使用TDD可以真正加强所有这些检查的质量(特别是使用Fuzz Testing)。


有趣的是。但是,如果我在运行时检查中漏掉了某些内容,我仍然会遇到麻烦的异常,不是吗?这不等于我编写测试很糟糕吗?那么我只需要确保无效数据在入口点正确处理,而不是在整个应用程序中彻底处理。 - RobSullivan

0

你的优缺点列表中缺少一项非常重要的内容,这使得单元测试比疯狂参数检查更加安全。

你只需要考虑“何时”和“何地”。

对于单元测试,“何时”和“何地”是:

  • 何时:在设计时
  • 何地:在应用程序代码之外的专用源文件中

对于过度检查数据,它们是:

  • 何时:在运行时
  • 何地:纠缠在应用程序源代码中,通常使用断言。

这就是重点:通过单元测试覆盖的代码可以在设计时检测到错误,当您运行测试时,如果您是偏执和精神分裂类型的测试人员(最好的),则编写旨在破坏任何可能性的测试,检查每个数据边界和恶意输入。您还可以使用代码覆盖工具确保测试了每个替代方案的每个分支。您没有限制:测试位于它们自己的文件中,不会混杂应用程序。无论您获得的测试行数比实际应用程序代码多十倍,都没有运行时惩罚,也没有可读性惩罚。

另一方面,过度的集成测试可以在运行时检测到错误。最糟糕的情况是它会在用户系统上检测到错误,而你对此无能为力(即使你曾经听说过这种错误发生)。即使你是多疑的人,你也必须限制你的测试。断言不能占应用程序代码的90%。这会引起可读性问题、维护问题和通常会有严重的性能惩罚。那么你会在哪里停止:只检查外部输入的参数?检查每个内部函数的每个可能或不可能的输入?检查每个循环不变量?还要测试当流程数据(全局变量、系统文件等)被更改时的行为吗?你还必须意识到断言代码也可能包含一些错误。如果一个断言的公式执行除法,那怎么办?你必须确保它不会导致除以零或类似的错误吗?
另一个问题是,在许多情况下,你不知道当断言失败时该做什么。如果你在一个真正的入口点,你可以返回一些可理解的东西给你的用户或库用户...当你检查内部函数时。

请理解我的回答有些夸张,我在夸大各种方法的优缺点。但我相信基本观点是正确的。 - kriss

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接