TDD 与防御性编程

TDD vs Defensive Programming

鲍勃叔叔说:

"Defensive programming, in non-public APIs, is a smell, and a symptom, of teams that don't do TDD."

我想知道 TDD 如何避免以非预期方式使用(内部)函数?我认为 TDD 无法避免它。它仅表明函数被正确使用,因为调用函数已通过单元测试。

在使用(非防御性)函数开发新功能时,此功能也是使用 TDD 开发的。因此,无意中使用该功能将导致新功能测试失败。

因此使用 TDD 驱动新功能将迫使您正确使用(内部)功能。

你认为 Bob 叔叔的推文是这个意思吗?

当您正确使用 TDD 时,您涵盖了所有可能的情况,并断言调用私有函数的 public 函数确实如预期的那样正确响应,不仅对快乐的场景,而且对所有不同的可能场景.当您在私有方法中使用防御编程时,您实际上是在为上述这些(不同的可能)场景做好准备。

我个人认为即使在私有方法中捍卫编程也不是坏事,但是,根据我上面的描述,我认为这是不必要的双重努力,而且它消除了TTD,因为您通过使代码复杂化来处理应用程序中的这些特殊情况,而不是以一种证明的方式编写代码。

So using TDD to drive new features will force you to correctly use (internal) functions.

没错。但请记住此处微妙的 "gap":您应该使用 TDD 编写(单元)测试来测试 public[ 的 contract 方法。您不关心这些方法的实现 - 那是所有内部实现细节。

因此:如果您的 "new" 代码以非预期的方式使用了现有方法,那么您 "told" 因为抛出了异常或您收到了意外结果。

这就是我所说的"gap":你看,上面描述了一种黑盒测试方法。您有一个 public 方法 X,并且您验证了它的 public 契约。将其与 白盒 测试进行比较,您在其中编写测试以覆盖 X 中采用的所有路径。这样做时,您会注意到:"ok to test that one condition in my internal method, I would have to drive this special data"。

但如前所述 - 我认为你应该进行 black 盒测试 - white 盒测试在重构内部方法时可能很容易崩溃。

这里还有一个附加维度:请记住,理想情况下,您更改代码以实现新功能。这意味着 添加 新功能只能通过编写 new classes 和方法来实现。这意味着您的 new 代码没有机会使用 private 内部方法。因为你在newclass内。换句话说:当您经常 运行 遇到以多种不同方式使用您的内部方法的情况时 - 那么您可能正在做一些 错误的事情

理想的路径是:通过创建一组新的 classes 来实现新需求。稍后,您必须 添加 其他要求 - 通过编写 more classes。

在那条理想的道路上——没有需要在内部方法中进行防御性编程。因为您完全了解此类内部方法的每个用例!

因此,结论是:避免在内部方法中进行防御性编程。确保您的 public API 检查所有先决条件,以便在出现问题时(尽可能快地)失败。尽量避免这些内部一致性检查——因为它们会使你的代码膨胀——请放心:在 5 周或 5 个月内,你将不记得你是否真的需要 那个检查,或者它是否只是 "defensive".

回答这个问题的一种方法是看看鲍勃叔叔就这个话题还说了些什么。 For example:

In a system with meager code coverage, few tests, and lots of tangled legacy code, defensive programming should be the rule.

In a system born of TDD, with 90+% coverage and highly reliable, well-maintained unit tests, defensive programming should be the exception.

由此,我们可以推断出他的主要论点——如果防御性检查确实提供了好处,那么这暗示我们缺少一些限制。如果我们遗漏了一些约束,并且所有测试都通过了,那么我们一定也遗漏了一些测试。

或者,以稍微不同的方式表达相同的想法——您的实施中的防御模式隐含的约束更接近边界(即,在 public API 中) .

如果有约束,例如限制允许哪些数据通过边界,那么应该有测试以确保边界实际执行约束。