TDD是否会使防御性编程变得多余?


104

今天,我与一位同事进行了有趣的讨论。

我是一名防御性程序员。我认为必须始终遵循“ 类必须确保其对象在从类外部进行交互时具有有效状态 ”的规则。该规则的原因是该类不知道其用户是谁,并且在以非法方式与之交互时,它应该可以预见地失败。我认为该规则适用于所有阶层。

在今天我进行讨论的特定情况下,我编写了代码来验证构造函数的参数正确(例如,整数参数必须大于0),并且如果不满足前提条件,则会引发异常。另一方面,我的同事认为这种检查是多余的,因为单元测试应该捕获该类的任何不正确使用。此外,他认为防御性编程验证也应该进行单元测试,因此防御性编程会增加很多工作,因此对于TDD并不是最佳的。

TDD是否能够取代防御性编程,这是真的吗?结果是否不需要参数验证(并不是我的意思是用户输入)?还是两种技术相辅相成?


120
您将未经单元构造检查的完全经过单元测试的库交给客户端使用,这将破坏类协定。您现在对那些单元测试有什么好处?
罗伯特·哈维

42
IMO是另一回事。防御性编程,适当的前提条件和前提条件以及丰富的类型系统使测试变得多余。
gardenhead's

37
我可以发表一个只说“悲伤”的答案吗?防御性编程可在运行时保护系统。测试会检查测试人员可以想到的所有潜在运行时条件,包括传递给构造函数和其他方法的无效参数。测试完成后将确认运行时行为符合预期,包括引发了适当的异常或传递无效参数时,会发生其他故意行为。但是测试并不能在运行时保护系统。
克雷格

16
“单元测试应捕获该类的任何不正确使用”-嗯,如何?单元测试将向您显示给定正确参数以及给定错误参数时的行为;他们无法向您展示将要给出的所有论点。
OJFord

34
我认为我没有看到一个更好的例子,说明关于软件开发的教条式思考如何导致有害的结论。
sdenham

Answers:


196

这是荒谬的。TDD强制代码通过测试,并强制所有代码对其进行一些测试。这不会阻止您的使用者错误地调用代码,也不会神奇地防止程序员丢失测试用例。

没有方法可以强迫用户正确使用代码。

还有就是要进行,如果你完全没有TDD你会陷入一个测试用例你> 0检查,实施它之前,并解决了这个轻微的论点-可能是由您添加检查。但是,如果您执行了TDD,则您的要求(在构造函数中> 0)将首先显示为失败的测试用例。因此,在添加支票后给您测试。

测试某些防御条件也是合理的(您添加了逻辑,为什么不想要测试那么容易测试的东西?)。我不确定为什么您似乎不同意这一点。

还是两种技术相辅相成?

TDD将开发测试。实施参数验证将使它们通过。


7
我不同意应该测试先决条件验证的观点,但是我确实不同意同事的观点,即需要测试先决条件验证而引起的额外工作是不首先创建先决条件验证的论点。地点。我已编辑我的帖子以澄清。
user2180613

20
@ user2180613创建一个测试,以测试前提条件的失败是否得到了适当处理:现在添加检查不是“额外的”工作,TDD要求将其变为绿色才是工作。如果您的同事认为您应该进行测试,观察到测试失败,然后再执行前提条件检查,那么从TDD-纯粹主义者的角度来看,他可能有一点要点。如果他只是说要完全忽略检查,那他就是傻了。TDD中没有任何内容表明您不能主动编写潜在故障模式的测试。
RM

4
@RM您不是要编写用于测试前提条件检查的测试。您正在编写测试以测试所调用代码的预期正确行为。从测试的角度来看,前提条件检查是一个不透明的实现细节,可以确保正确的行为。如果您想找到一种更好的方法来确保所调用代码中的正确状态,请执行此操作,而不要使用传统的前提条件检查。该测试将证明您是否成功,并且仍然不知道或不在乎您是如何做到的。
克雷格

@ user2180613这是一些令人敬畏的理由:D如果编写软件的目标是减少需要编写和运行的测试数量,则不要编写任何软件-零测试!
Gusdor

3
这个答案的最后一句话很明确。
罗伯特·格兰特

32

防御性编程和单元测试是两种捕获错误的不同方法,每种方法都有不同的优势。仅使用一种检测错误的方法会使您的错误检测机制变得脆弱。两者都使用将捕获一个或多个可能遗漏的错误,即使在不是面向公众的API的代码中也是如此;例如,有人可能忘记为传递到公共API中的无效数据添加单元测试。在适当的地方检查所有内容意味着发现错误的机会更大。

在信息安全中,这称为深度防御。进行多层防御可以确保即使其中一个失败,也有其他人可以抓住。

您的同事对一件事是正确的:您应该测试您的验证,但这不是“不必要的工作”。它与测试任何其他代码相同,您要确保所有用法(即使是无效用法)都具有预期的结果。


说参数验证是前提条件验证的一种形式,而单元测试是事后条件验证,这是为什么它们相互补充的说法是否正确?
user2180613

1
“这与测试任何其他代码相同,您要确保所有用法,甚至无效的用法都具有预期的结果。” 这个。没有经过设计的传递输入时,任何代码都不能传递。这违反了“快速失败”原则,并且可能使调试成为噩梦。
jpmc26 2016年

@ user2180613-并非如此,但更多的是单元测试检查开发人员期望的失败条件,而防御性编程技术则检查开发人员不期望的条件。单元测试用于验证前提条件(通过使用注入到检查前提条件的调用方的模拟对象)。
Periata Breatta

1
@ jpmc26是的,失败是测试的“预期结果”。您进行测试以表明它失败了,而不是无声地表现出一些未定义(意外)的行为。
KRyan

6
TDD在您自己的代码中捕获错误,而防御性编程则在其他人的代码中捕获错误。因此,TDD可以帮助您确保足够的防御能力:)
jwenting 16/09/25

30

TDD绝对不能取代防御性编程。相反,您可以使用TDD来确保所有防御措施均已就位并且可以按预期工作。

在TDD中,您不应该先编写测试就编写代码-认真遵循红绿重构周期。这意味着,如果要添加验证,请首先编写一个需要此验证的测试。用负数和零调用有问题的方法,并期望它引发异常。

同样,不要忘记“重构”步骤。尽管TDD是测试驱动的,但这并不意味着测试。您仍然应该应用适当的设计,并编写明智的代码。编写防御性代码是明智的代码,因为它使期望更加明确,并且代码总体上更加健壮–尽早发现可能的错误使其易于调试。

但是我们不是应该使用测试来定位错误吗?断言和测试是互补的。一个好的测试策略将混合使用各种方法来确保软件的健壮性。仅单元测试或仅集成测试或仅代码中的声明都不能令人满意,您需要一个良好的组合来以可接受的努力使您的软件具有足够的信心。

再有就是你的同事的一个非常大的概念上的误区:单元测试不能测试用途类的,只有类本身工作正常孤立。您将使用集成测试来检查各个组件之间的交互是否正常,但是可能的测试用例的组合爆炸使得无法测试所有内容。因此,集成测试应将自己限制在几个重要的案例中。同时涵盖边缘情况和错误情况的更详细的测试更适合于单元测试。


16

有测试来支持和确保防御性编程

防御性编程可在运行时保护系统的完整性。

测试是(通常是静态的)诊断工具。在运行时,您的测试遥遥无期。它们就像用来搭建高砖墙或岩石圆顶的脚手架。您不会在结构中留下重要部分,因为在施工过程中有脚手架将其支撑起来。在施工过程中,您有一个脚手架将其支撑起来,以方便放入所有重要的零件。

编辑:一个比喻

类似于代码中的注释呢?

评论有其目的,但可能多余,甚至有害。例如,如果您将有关代码的内在知识放入注释中,然后更改代码,则注释最多变得无关紧要,最坏时变得有害。

因此,说您在测试中投入了很多代码基础知识,例如MethodA不能为null,而MethodB的参数必须为> 0。然后代码更改。现在对于A来说可以为Null,而B可以取小到-10的值。现有的测试现在在功能上是错误的,但将继续通过。

是的,您应该在更新代码的同时更新测试。您还应该在更新代码的同时更新(或删除)注释。但是我们都知道这些事情并不总是会发生,并且会犯错误。

这些测试可以验证系统的行为。实际行为是系统本身固有的,而不是测试固有的。

可能出什么问题了?

有关测试的目标是考虑所有可能出问题的地方,为其编写测试以检查正确的行为,然后编写运行时代码,使其通过所有测试。

这意味着防御性编程才是重点

如果测试很全面,TDD会推动防御性编程。

更多测试,推动更具防御性的编程

当不可避免地发现错误时,将编写更多测试以对表现该错误的条件进行建模。然后固定代码,使这些测试通过,新测试保留在测试套件中。

一组好的测试将把好的和坏的参数都传递给函数/方法,并期望结果一致。反过来,这意味着被测组件将使用前提条件检查(防御性编程)来确认传递给它的参数。

一般而言...

例如,如果特定过程的null参数无效,那么至少一个测试将通过null,并且它将期望某种“无效null参数”异常/错误。

当然,至少还有另一个测试将通过一个有效的参数(或循环通过一个大数组并传递多个有效的参数),并确认结果状态是否合适。

如果一个测试没有通过该null参数,并且被期望的异常拍打(并且该异常被抛出,因为代码防御性地检查了传递给它的状态),则该null可能最终被分配给一个类的属性或被掩埋在某种不应该的集合中。

在软件出厂后的某个遥远地理位置,这可能会在类实例传递到的系统的某些完全不同的部分中导致意外行为。那是我们实际上要避免的事情,对吧?

甚至可能更糟。具有无效状态的类实例可以被序列化和存储,仅当将其重构以供以后使用时才导致失败。Geez,我不知道,也许它是某种机械控制系统,在关机后无法重启,因为它无法反序列化其自身的持久配置状态。或者,可以将类实例序列化并传递给其他实体创建的完全不同的系统,并且系统可能崩溃。

尤其是如果其他系统的程序员没有防御性的代码。


2
太好笑了,降票速度如此之快,以至于现在绝对有办法让降票者阅读第一段之后的内容。
Craig

1
:-)我只是投票而没有阅读第一段,因此希望可以使平衡...
SusanW

1
看来我至少可以做到:-)(实际上,我确实阅读了其余内容以确保。一定不要草率-尤其是在这样的话题上!)
SusanW

1
我想你大概有。:)
克雷格

可以使用诸如代码合同之类的工具在编译时进行防御性检查。
马修·怀特

9

我们通常不使用TDD来讨论“软件测试”,而通常不使用“防御性编程”,而要谈论我最喜欢的进行防御性编程的方法,即使用断言。


因此,既然我们进行软件测试,就应该退出在生产代码中放置断言语句,对吗?让我计算一下这是错误的方式:

  1. 断言是可选的,因此,如果您不喜欢它们,则可以在禁用断言的情况下运行系统。

  2. 断言检查测试无法(也不应这样做)的事情。因为测试应该具有系统的黑盒视图,而断言却具有白盒视图。(当然,因为他们生活在里面。)

  3. 断言是出色的文档工具。没有任何评论曾经或将不会像断言同一件事的一段代码那样明确。另外,随着代码的发展,文档趋于过时,并且编译器无法以任何方式强制执行。

  4. 断言可以捕获测试代码中的错误。您是否曾经遇到过测试失败且您不知道谁是错误的情况-生产代码或测试?

  5. 断言可能比测试更相关。测试将检查功能需求中规定的内容,但是代码通常必须做出某些假设,而这些假设要比这些假设更具技术性。编写功能需求文档的人很少想到除以零。

  6. 断言指出了只能大致暗示的错误。因此,您的测试设置了一些广泛的前提条件,调用了一些冗长的代码,收集了结果,并发现它们与预期的不同。如果有足够的疑难解答,您最终将确切找到问题出在哪里,但是断言通常会首先找到它。

  7. 断言可降低程序复杂度。您编写的每一行代码都会增加程序的复杂性。断言和finalreadonly)关键字是我所知道的唯一两个可以真正降低程序复杂度的构造。那是无价的。

  8. 断言可帮助编译器更好地理解您的代码。请在家尝试一下:void foo( Object x ) { assert x != null; if( x == null ) { } }编译器应发出警告,告诉您条件x == null始终为假。那可能非常有用。

以上是我的博客2014-09-21“断言和测试”中的帖子摘要


我想我大多不同意这个答案。(5)在TDD中,测试套件是规范。您应该编写使测试通过的最简单的代码,仅此而已。(4)红绿色工作流程可确保测试在应有的情况下失败,并在存在预期的功能时通过。断言在这里没有太大帮助。(3,7)文档是文档,而声明不是。但是通过明确假设,代码变得更加自我记录。我认为它们是可执行的注释。(2)白盒测试可以成为有效测试策略的一部分。
阿蒙

5
“在TDD中,测试套件就是规范。您应该编写使测试通过的最简单的代码,仅此而已。”:我不认为这总是一个好主意:正如答案中指出的那样,代码中可能要验证的其他内部假设。相互抵消的内部错误呢?您的测试通过了,但是代码中的一些假设是错误的,以后可能导致隐患。
Giorgio

5

我相信大多数答案都缺少一个关键的区别:这取决于如何使用您的代码。

所讨论的模块是否将由您正在测试的应用程序的其他客户端使用?如果要提供供第三方使用的库或API,则无法确保它们仅使用有效输入来调用代码。您必须验证所有输入。

但是,如果所讨论的模块仅由您控制的代码使用,那么您的朋友可能会有一点建议。您可以使用单元测试来验证是否仅使用有效输入调用了该模块。前提条件检查仍然可以被认为是一个很好的做法,但它是一个权衡:我给你垃圾这将检查你的条件代码知道不可能出现,它只是掩盖了代码的意图。

我不同意前提条件检查需要更多的单元测试。如果您决定不需要测试某些形式的无效输入,则该函数是否包含前提条件检查都没有关系。记住测试应该验证行为,而不是实现细节。


4
如果所调用的过程没有验证输入的有效性(这是原始争论),则您的单元测试将无法确保仅使用有效输入来调用相关模块。特别是,它可能使用无效的输入来调用,但无论如何在测试案例中无论如何都会返回正确的结果-各种类型的未定义行为,溢出处理等都可能在禁用优化的测试环境中返回预期结果,但生产失败。
彼得斯(Peteris)2016年

@Peteris:您是否正在考虑像C中那样的未定义行为?在不同的环境中调用具有不同结果的未定义行为显然是一个错误,但是也不能通过前提条件检查来阻止。例如,如何检查指向有效内存的指针参数?
JacquesB

3
这仅适用于最小的商店。一旦您的团队超过了六个人,您将仍然需要进行验证检查。
罗伯特·哈维

1
@RobertHarvey:在这种情况下,应将系统划分为具有定义明确的接口的子系统,并在该接口上执行输入验证。
JacquesB '16

这个。这取决于代码,团队会使用此代码吗?团队可以访问源代码吗?如果纯粹是内部代码,那么检查参数可能只是一个负担,例如,您先检查0然后抛出异常,然后调用者查看该类的代码可以抛出异常等,然后等待..在这种情况下,对象永远不会收到0,因为它们之前已被滤除2个lvls。如果那是第三方要使用的库代码,那就是另一个故事。并非所有代码都被全世界使用。
Aleksander Fular

3

这种论点让我感到困惑,因为当我开始练习TDD时,我的形式为“对象在<无效输入>时响应<确定方式>”的单元测试增加了2或3倍。我想知道您的同事如何在不进行功能验证的情况下成功通过此类单元测试。

相反,单元测试表明您绝不会产生会传递给其他函数参数的错误输出,这很难证明。与第一种情况一样,它在很大程度上取决于对边缘情况的彻底覆盖,但是您还具有其他要求,即所有功能输入必须来自您已经对其单元进行了测试的其他功能的输出,而不是来自用户输入或第三方模块。

换句话说,TDD所做的并不是阻止您需要验证代码,而不会帮助您避免忘记代码。


2

我认为我对您同事的言论的解释与其余大多数答案不同。

在我看来,论点是:

  • 我们所有的代码都经过了单元测试。
  • 所有使用您组件的代码都是我们的代码,或者如果没有,则由其他人进行单元测试(未明确说明,但这是我从“单元测试应捕获该类的任何不正确使用”中了解到的)。
  • 因此,对于函数的每个调用者,在某个地方可以进行单元测试以模拟您的组件,并且如果调用者将无效值传递给该模拟,则测试将失败。
  • 因此,函数传递无效值时执行什么操作都没有关系,因为我们的测试表明这不可能发生。

对我来说,这种说法具有一定逻辑性,但是过于依赖单元测试来涵盖所有可能的情况。一个简单的事实是,100%的行/分支/路径覆盖率并不一定行使呼叫者可能传递的所有,而100%覆盖呼叫者的所有可能状态(也就是说,其输入的所有可能值)和变量)在计算上是不可行的。

因此,我倾向于倾向于对调用方进行单元测试,以确保(就测试而言)它们永远不会传递错误的值,并且另外要求当传递错误的值时,您的组件以某种可识别的方式失败(至少在可能的范围内以您选择的语言来识别不良价值)。这将有助于在集成测试中出现问题时进行调试,并且同样可以帮助您的类中任何不那么严格地将其代码单元与该依赖关系分离的用户。

不过请注意,如果在传递值<= 0时记录并测试函数的行为,则负值将不再无效(至少,对于而言throw,任何参数都无效),因为那样也记录了引发异常!)。呼叫者有权依靠这种防御行为。语言允许的话,这可能是因为这是在任何情况下的最佳方案-该功能没有“无效输入”,但谁想到不刺激功能分为抛出一个异常应该是单元测试充分来电,以确保他们不” t传递任何导致该值的值。

尽管认为您的同事比大多数答案都不太完全错,但我得出的结论是,这两种技术是相辅相成的。进行防御性编程,记录防御性检查并进行测试。如果您的代码用户在犯错误时无法从有用的错误消息中受益,则这项工作只是“不必要的”。从理论上讲,如果他们在将所有代码与您的代码集成之前对其进行了全面的单元测试,并且他们的测试中永远没有任何错误,那么他们将永远不会看到错误消息。实际上,即使他们正在执行TDD和完全依赖注入,他们仍然可能在开发过程中进行探索,或者测试可能会失效。结果是他们在代码完美之前就调用了您的代码!


强调测试调用者以确保它们不会传递错误值的工作似乎使自己适用于易碎的代码,这些代码具有大量的低音处理依赖,并且没有明确的关注点分离。我真的不希望使用该方法背后的思想所产生的代码。
克雷格

@Craig:这样看,如果您通过模拟组件的依赖关系隔离了要测试的组件,那么为什么测试它仅将正确的值传递给那些依赖项呢?而且,如果您无法隔离组件,您是否真的将关注点分开了?我并不不同意防御性编码,但是如果防御性检查是测试调用代码正确性的手段,那将是一团糟。因此,我认为提问者的同事是正确的,因为检查是多余的,但将其视为不写检查的理由是错误的:-)
Steve Jessop

我看到的唯一明显的漏洞是我仍在测试我自己的组件不能将无效值传递给那些依赖项,我完全同意应该这样做,但是要由多少业务经理做出多少决定才能私有化?组件公开,以便合作伙伴可以调用吗?这实际上使我想起了数据库设计以及当前与ORM的所有往来,导致许多(大多数是年轻的)人宣称数据库只是愚蠢的网络存储,不应使用约束,外键和存储过程来保护自己。
Craig

我看到的另一件事是,在这种情况下,当然是,您仅测试对模拟的调用,而不是对实际依赖项的测试。最终,那些依赖项中的代码不能或不能正确地与特定的传递值一起工作,而不是调用者中的代码。因此,依赖项需要做正确的事情,并且需要对依赖项进行足够的独立测试覆盖,以确保它可以做到。请记住,我们正在谈论的这些测试称为“单元”测试。每个依赖项都是一个单位。:)
克雷格

1

公共接口可以并且将被滥用

对于任何非私有接口,您的同事“单元测试应捕获类的任何不正确使用”的主张严格是错误的。如果可以使用整数参数来调用公共函数,则可以并且将使用任何整数参数来调用该公共函数,并且代码应具有适当的行为。如果公共功能签名接受例如Java Double类型,则null,NaN,MAX_VALUE和-Inf都是可能的值。单元测试无法赶上类的不正确使用,因为这些测试不能测试将使用这个类的代码,因为该代码还没有写,可能不是由你来写的,肯定会的范围之内单元测试。

另一方面,此方法可能对(希望有更多的)私有属性有效-如果一个类可以确保某个事实始终为真(例如,属性X永远不能为null,整数位置不超过最大长度) ,当调用函数A时,所有必要的数据结构均已正确形成),因此可以避免由于性能原因而一次又一次地验证它,而改为依赖单元测试。


标题和第一段是正确的,因为不是单元测试将在运行时执行代码。不管其他任何运行时代码以及不断变化的实际条件以及不良的用户输入和黑客尝试,均会与代码交互。
克雷格(Craig)

1

防止滥用是一项功能,由于需要而开发。(并非所有接口都需要对滥用进行严格检查;例如,使用范围很窄的内部接口。)

该功能需要进行测试:针对滥用的防御措施是否有效?测试此功能的目的是试图证明它没有:试图对模块的某些误用进行检查,以防止其误用。

如果特定检查是必需的功能,则断言某些测试的存在使其不必要是不明智的。如果某个函数的某个功能(例如)在参数3为负数时抛出异常,则这是不可协商的;它应该这样做。

但是,我怀疑从这样一种情况来看,您的同事实际上是有意义的,在这种情况下,无需对输入进行特定的检查,而对不良输入进行特定的响应:在这种情况下,对于健壮性。

检查是否存在某些顶级功能,部分是为了保护某些弱的或未经过良好测试的内部代码免受意外的参数组合的影响(这样,如果对代码进行了良好的测试,则不必进行检查:代码可以只是“天气”的错误参数)。

同事的想法中有一个真理,他可能的意思是:如果我们从非常健壮的低级代码中构建功能,这些低级代码经过防御性编码并针对所有滥用情况进行了单独测试,则可能存在高级别功能强大而又无需进行广泛的自我检查。

如果违反了它的合同,那么它将转化为对低级功能的某种滥用,可能是通过引发异常或其他任何方式。

唯一的问题是较低级别的异常不是特定于较高级别的接口的。这是否有问题取决于要求。如果要求仅仅是“该函数应具有较强的鲁棒性,以防止滥用,并抛出某种异常而不是崩溃,或者继续使用垃圾数据进行计算”,那么实际上,该函数可能会被其下层的所有鲁棒性所覆盖。内置的。

如果该功能需要与其参数相关的非常具体,详细的错误报告,则较低级别的检查不能完全满足这些要求。它们仅确保函数以某种方式崩溃(不会继续使用错误的参数组合,从而产生垃圾结果)。如果编写客户端代码来专门捕获某些错误并进行处理,则可能无法正常工作。客户端代码本身可能正在获取参数所基于的数据作为输入,并且可能期望函数检查这些参数并将错误值转换为记录的特定错误(以便它可以处理这些错误)。错误),而不是其他一些无法处理的错误,甚至可能会停止软件映像。

TL; DR:你的同事可能不是白痴。你们只是围绕同一件事以不同的观点彼此交谈,因为这些要求没有被完全确定,并且每个人对什么是“未成文的要求”都有不同的想法。您认为,当对参数检查没有特定要求时,无论如何您应该编写详细的检查代码;同事认为,只要参数错误,就让强大的低级代码崩溃。通过代码争论未成文的需求在某种程度上是徒劳的:认识到您不同意需求而不是代码。您的编码方式反映了您认为的要求;同事的方式代表了他对需求的看法。如果您这样看,很明显,对与错是“ 在代码本身中;该代码仅代表您对规范的看法。


这与处理可能是宽松要求的一般哲学困难有关。如果在给定格式错误的输入时允许某个功能显着但不能完全自由支配,则可以任意发挥作用(例如,如果可以保证某个图像解码器可以满足要求,则可以(随意)生成任意像素组合或异常终止) ,但是如果它可能允许恶意制作的输入执行任意代码,则不会这样做),可能尚不清楚哪种测试用例适合于确保没有任何输入产生不可接受的行为。
超级猫

1

测试定义了您的班级合同。

结果,没有测试就定义了一个包含未定义行为的契约。因此,当您传递nullFoo::Frobnicate(Widget widget),并且发生了难以置信的运行时破坏时,您仍在您所在课程的合同范围内。

稍后您决定:“我们不希望出现不确定的行为”,这是一个明智的选择。这意味着您必须具有传递null给的预期行为Foo::Frobnicate(Widget widget)

然后,您通过添加一个

[Test]
void Foo_FrobnicatesANullWidget_ThrowsInvalidArgument() 
{
    Given(Foo foo);
    When(foo.Frobnicate(null));
    Then(Expect_Exception(InvalidArgument));
}

1

一组很好的测试将练习您类的外部接口,并确保此类滥用会产生正确的响应(异常,或您定义为“正确”的任何东西)。实际上,我为类编写的第一个测试用例是使用超出范围的参数调用其构造函数。

倾向于通过完全单元测试的方法消除的防御性编程是对内部不变式的不必要验证,这些不变式不能被外部代码违反。

我有时采用的一个有用的想法是提供一种测试对象不变性的方法。您的拆卸方法可以调用它来验证您对对象的外部操作不会破坏不变式。


0

TDD的测试将在代码开发过程中发现错误。

您描述为防御性编程的一部分的边界检查将在使用代码时捕获错误。

如果这两个域是相同的,即您正在编写的代码仅由该特定项目在内部使用,则TDD可能确实会排除您描述的防御性编程界限的必要性,但前提是这些类型在TDD测试中专门执行边界检查


作为一个具体示例,假设使用TDD开发了财务代码库。其中一项测试可能断言特定值永远不会为负。这样可以确保库的开发人员在实现功能时不会意外滥用类。

但是,在发布库并在自己的程序中使用它之后,那些TDD测试并不会阻止我分配负值(假设它是公开的)。边界检查会。

我的观点是,如果TDD断言可以解决负值问题,但前提是该代码只能在更大的应用程序的开发过程中内部使用(在TDD下),但如果该代码将成为其他没有TDD的程序员使用的库,框架和测试,范围检查事项。


1
我没有拒绝表决,但我同意反对表决的前提是,对这种论点添加微妙的区别会使水变得混乱。
Craig

@Craig我会对您对我添加的特定示例的反馈意见感兴趣。
Blackhawk

我喜欢这个例子的特殊性。我仍然唯一关心的是整个论点的普遍性。例如; 随之而来的是团队中的一些新开发人员,并编写了使用该财务模块的新组件。新手并没有意识到系统的所有复杂性,更不用说关于系统应该如何运行的各种专家知识都嵌入在测试中,而不是被测试的代码中。
克雷格

因此,新手/ gal错过了创建一些重要测试的机会,并且最终导致测试的冗余-系统不同部分中的测试正在检查相同的条件,并且随着时间的流逝而变得不一致,而不仅仅是放置适当的断言和前提条件在操作所在的代码中进行检查。
克雷格(Craig)2016年

1
这样的事情。除了这里的许多争论是关于让调用代码的测试进行所有检查之外。但是,如果您完全有某种程度的扇入,那么最终您会在许多不同的地方进行相同的检查,这本身就是维护问题。如果某个过程的有效输入范围发生了变化,但是您拥有针对使用不同组件的测试所内置的该范围的领域知识,该怎么办?我仍然完全赞成防御性编程,并使用性能分析来确定是否以及何时遇到性能问题。
Craig

0

TDD和防御性编程并驾齐驱。两者并用不是多余的,但实际上是互补的。当您拥有一个函数时,您要确保该函数按说明工作并为它编写测试;如果您不了解在输入错误,返回错误,状态错误等情况下发生的情况,那么您编写的测试不够鲁棒,即使所有测试都通过了,您的代码也会变得脆弱。

作为嵌入式工程师,我喜欢使用编写函数的示例来简单地将两个字节加在一起并返回如下结果:

uint8_t AddTwoBytes(uint8_t a, uint8_t b, uint8_t *sum); 

现在,如果您只是做到*(sum) = a + b这一点,那么它将起作用,但是只有一些输入。a = 1并且b = 2会做sum = 3; 然而,由于金额的大小是一个字节,a = 100并且b = 200会使得sum = 44由于溢出。在C语言中,在这种情况下,您将返回错误以表示函数失败;在代码中抛出异常是相同的。不考虑失败或测试如何处理它们将无法长期运行,因为如果发生这些情况,将无法对其进行处理,并且可能会导致许多问题。


看起来像一个很好的采访问题示例(为什么它有一个返回值和一个“出”参数- sum空指针会发生什么?)。
Toby Speight
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.