6

我们有一个约定来验证构造函数和公共函数/方法的所有参数。对于引用类型的强制参数,我们主要检查非空,这是构造函数中的主要验证,我们在其中设置类型的强制依赖项。

我们这样做的第一个原因是及早捕获该错误,并且在不知道引入错误参数的位置或时间的情况下几个小时后不会得到空引用异常。随着我们开始过渡到越来越多的 TDD,一些团队成员觉得验证是多余的。

大力提倡 TDD 的 Bob 叔叔强烈建议不要进行参数验证。他的主要论点似乎是“我有一套单元测试可以确保一切正常”

但我终其一生都看不到单元测试能以何种方式阻止我们的开发人员在生产代码中使用错误的参数调用这些方法。

拜托,那里的单元测试人员,如果你能用具体的例子以合理的方式向我解释这一点,我会非常乐意抓住这个参数验证!

4

5 回答 5

7

我的回答是“不能”。基本上这听起来我不同意鲍勃叔叔(除其他外)。

很容易想象这样一种情况,您已经对非空参数的库代码进行了单元测试,并且您已经对调用代码的路径进行了单元测试,该路径恰好在您不知道的情况下向库提供了空参数它,但恰好不会对该特定路径造成任何问题。你可以有 100% 的覆盖率,实际上是一组相当不错的测试,但仍然没有注意到问题。

一切都好吗?不,当然不是——因为你在不知情的情况下违反了图书馆合同(不要给我一个非空值)。您是否可以放心,您提供空参数的唯一情况是无关紧要的情况?我不这么认为——特别是如果你甚至不知道这个论点是空的。

在我看来,公共 API 应该验证它们的参数,而不管调用代码和 API 本身是否经过单元测试。调用代码的问题要暴露出来,尽早暴露出来。

于 2012-06-24T21:18:44.290 回答
3

这是我多年来一直在问自己的问题,但仍然没有得到令人满意的答案。

但是我认为,在进行参数验证时,您需要区分两种情况:

  1. 您是否正在验证参数以捕获逻辑编程错误?

    if (foo == null) throw new ArgumentNullException("foo");
    

    很可能就是一个例子。

  2. 您是否正在验证参数,因为它是一些外部输入(由用户提供,或从配置文件或从数据库中读取),它可能是无效的并且必须被拒绝?

    if (customerDateOfBirth == new DateTime(1900, 1, 1)) throw …;
    

    可能属于这种类型的参数检查。

(如果您要公开团队外部人员使用的 API,第 2 点也大致适用。)

我怀疑诸如单元测试、合同设计以及在某种程度上“早期失败”之类的方法主要集中在第一种类型的参数验证上。也就是说,它们试图检测逻辑编程错误,而不是无效输入。

如果是这种情况,那么我敢说您遵循哪种错误检测方法实际上并不重要。每个都有自己的优点和缺点。在极端情况下(例如,当您完全信任自己编写无错误代码的能力时),您甚至可以完全放弃这些检查。

但是,无论您选择哪种方法来检测代码中的逻辑错误,您仍然需要验证用户输入等,因此需要区分两种参数检查。


†)一个业余爱好者在比较按合同设计、单元测试和“早期失败”的相对优缺点方面的不完整尝试:

(虽然你没有要求它......我只会提到一些关键的区别。)

早期失败(例如在方法开始时显式验证参数):

  • 编写防卫等基本检查null很容易编写
  • 可能会混淆对逻辑错误的防范和使用相同语法对外部输入的验证
  • 不允许您测试方法的交互
  • 不鼓励您严格定义(并因此考虑)您的方法的合同

单元测试:

  • 允许您单独测试代码,而无需运行实际应用程序,因此可以更快地检测错误
  • 如果发生逻辑错误,您无需跟踪堆栈即可找到原因,因为每个单元测试都代表代码的特定“用例”。
  • 不仅可以测试单个方法,甚至可以测试多个对象之间的交互(想想存根和模拟)
  • 编写简单的测试(例如防范null)比使用“早期失败”方法(如果您严格遵守 Arrange-Act-Assert 模式)更工作

按合同设计:

  • 强制你明确地声明你的类的契约(尽管这也可以通过单元测试来实现——只是以不同的方式)
  • 允许您轻松声明类不变量(必须始终成立的内部条件)
  • 不像其他方法那样受到许多编程语言/框架的支持
于 2012-06-24T21:12:31.450 回答
2

我几乎同意鲍勃叔叔的所有观点,但这不是这个。我投票支持“快速失败,努力失败”的政策。

于 2012-06-25T13:30:49.983 回答
2

这完全取决于您正在开发的应用程序的类型。

  1. 我大部分时间都在编写不公开公共 API 的应用程序,在这种情况下,应用程序必须是确定性的,从某种意义上说,所有参数都必须并且将不同于 null。简而言之,您应该在系统边界执行输入验证,不要让这些无效输入潜入您的应用程序,这可能会导致空引用等。在这种应用程序中,您可以完全控制在您获取应用程序的输入时检查它们。

  2. 如果您正在编写公共 API,则不建议检查空引用。只需查看所有可能引发异常的 MSDN 类方法,所有这些都发生在 API 内部作为前提条件检查,您可以阅读C# 框架设计指南以获取更多信息。

在我看来,无论是公开(或不公开)的 API 应用程序,为您的方法设置先决条件总是一件好事(这些合同是您的同行的文档,他们将在未来处理您的代码)

于 2012-06-24T21:20:34.360 回答
0

这与TDD无关。

对于公共 API,是的,我们应该尽可能快地进行参数检查。

所有构造函数参数检查对我来说似乎完全没有必要,因为团队之外的任何其他人都不会使用它。为什么我们有空检查?我们不信任调用这些方法的代码。

那么什么是公共 API?所有公共方法?如果是这样,那么我猜就没有所谓的内部 API。那么为什么要使用public这个词呢?为什么只说所有公共方法都应该进行空/边界检查。

我认为问题的根本原因是对我们自己的代码和团队成员缺乏信任,显然我们以错误的方式解决问题。

于 2012-06-27T15:12:20.647 回答