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

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

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

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

评论

您将未经单元构造检查的完全经过单元测试的库交给客户端使用,它们就会违反类协定。您现在对那些单元测试有什么好处?

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

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

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

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

#1 楼

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

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

有一点要说的是,如果您完美地完成了TDD,您将在实现之前在一个测试用例中捕获了大于0的检查,并通过添加检查来解决了这个问题。但是,如果您执行了TDD,则您的要求(在构造函数中> 0)将首先显示为失败的测试用例。因此,在添加支票后给您提供测试。

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


还是这两种技术相辅相成?


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

评论


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

–user2180613
16-09-23在22:46



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

– R.M.
16-09-23在23:07

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

– Craig
16 Sep 24 '17:53



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

– Gusdor
16-09-25在20:42

这个答案的最后一句话很明确。

–Rob Grant
16-09-26在10:11

#2 楼

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

在信息安全中,这称为深度防御。进行多层防御可以确保如果其中一项失败,那么还有其他方面可以阻止。

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

评论


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

–user2180613
16-09-23在22:42



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

– jpmc26
16 Sep 24'2:32



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

– Periata Breatta
16-09-24在8:28



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

– KRyan
16-09-24在13:47

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

– jwenting
2016年9月25日下午5:00

#3 楼

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

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

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

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

那么,对同事的概念上的误解:单元测试永远不能测试类的使用,只是单元本身可以按预期工作。您将使用集成测试来检查各个组件之间的交互是否起作用,但是可能的测试用例的组合爆炸使得无法测试所有内容。因此,集成测试应将自己限制在几个重要的案例中。涵盖边缘情况和错误情况的更详细的测试更适合单元测试。

#4 楼

可以通过测试来支持并确保防御性编程。
防御性编程可以在运行时保护系统的完整性。
测试是(主要是静态的)诊断工具。在运行时,您的测试遥遥无期。它们就像用来搭建高砖墙或岩石圆顶的脚手架。您不会在结构中留下重要的部分,因为在施工过程中有脚手架将其支撑起来。在构建过程中,您有一个脚手架将其支撑起来,以便于将所有重要的部分放入其中。
编辑:类比
与代码中的注释类比怎么样?
注释有其目的,但是可以多余甚至有害。例如,如果您将有关代码的内在知识放入注释中,然后更改代码,则注释最好变得无关紧要,最坏的情况下变得有害。
因此,您将代码库的许多内在知识放入了注释中。测试,例如MethodA不能为null,MethodB的参数必须为> 0。然后代码更改。现在对于A来说可以为Null,而B可以取小到-10的值。现在,现有测试在功能上是错误的,但仍会继续通过。
是的,您应该在更新代码的同时更新测试。您还应该在更新代码的同时更新(或删除)注释。但是我们都知道这些事情并不总是会发生,并且会出错。
测试可以验证系统的行为。实际行为是系统本身固有的,而不是测试固有的。
可能会出错的原因是什么?
关于测试的目标是思考所有可能出错的地方,编写一个测试为此,它会检查正确的行为,然后编写运行时代码,使其通过所有测试。
这意味着防御性编程才是关键。
如果测试很全面,TDD会驱动防御性编程。
更多测试,推动更多防御性编程
当不可避免地发现错误时,将编写更多测试以对表现该错误的条件进行建模。然后固定代码,使这些测试通过,然后将新测试保留在测试套件中。
一套好的测试将把好的和坏的参数同时传递给函数/方法,并期望一致的结果。反过来,这意味着被测试的组件将使用前提条件检查(防御性编程)来确认传递给它的参数。
一般来讲...
例如,如果特定对象的参数为null过程无效,那么至少一个测试将通过null,并且将期望某种“无效的null参数”异常/错误。
至少另一个测试将通过有效参数当然,-或遍历一个大数组并传递多个有效参数-并确认结果状态是否适当。
如果测试未通过该空参数并遭到期望的异常处理(并且之所以抛出该异常,是因为代码防御性地检查了传递给它的状态),那么null可能最终被分配给一个类的属性,或者被埋在不应包含的某种类型的集合中。
在类实例传递到的系统的某些完全不同的部分中导致意外行为,我软件出厂后,在某些遥远的地理位置。那是我们实际上要避免的事情,对吧?
甚至可能更糟。具有无效状态的类实例可以被序列化和存储,仅当重新构造供以后使用时才导致失败。 Geez,我不知道,也许它是某种机械控制系统,在关机后无法重启,因为它无法反序列化其自身的持久配置状态。或者可以将类实例序列化并传递给其他实体创建的完全不同的系统,该系统可能会崩溃。
尤其是如果其他系统的程序员没有防御性的代码。

评论


太好笑了,下降投票速度如此之快,以至于现在绝对有可能下降投票者可以阅读第一段之后的内容。

– Craig
16-09-24在7:03

:-)我只是投票而没有阅读第一段,所以希望可以平衡一下...

– SusanW
16-09-26在16:13

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

– SusanW
16-09-26在16:56

我想你大概有。 :)

– Craig
16 Sep 26 '16:58

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

–马修·怀特(Matthew Whited)
16-09-26在17:17

#5 楼

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


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


断言是可选的,因此,如果您不喜欢它们,则只需禁用断言即可运行系统。测试不能(也不应)的事情。因为测试应该具有系统的黑盒视图,而断言却具有系统的白盒视图。 (当然,因为他们生活在其中。)
断言是出色的文档工具。没有评论曾经或将永远不会像断言同一件事的一段代码那样明确。而且,随着代码的发展,文档趋于过时,并且编译器无法以任何方式强制执行。
断言可以捕获测试代码中的错误。您是否曾经遇到过测试失败的情况,并且您不知道谁错了?生产代码还是测试?
断言比测试更有意义。测试将检查功能需求中规定的内容,但是代码通常必须做出某些假设,而这些假设要比技术上要远得多。编写功能需求文档的人很少想到除以零。
断言指出了只能广泛暗示测试的错误。因此,您的测试设置了一些广泛的前提条件,调用了一些冗长的代码,收集了结果,发现结果与预期不符。如果有足够的故障排除方法,您最终将确切找到问题出在哪里,但是断言通常会首先找到它。
断言可降低程序复杂度。您编写的每一行代码都会增加程序的复杂性。断言和finalreadonly)关键字是我所知道的仅有的两种可真正降低程序复杂性的结构。这是无价的。
断言可以帮助编译器更好地理解您的代码。请在家尝试一下:void foo( Object x ) { assert x != null; if( x == null ) { } }您的编译器应发出警告,告诉您条件x == null始终为false。这可能非常有用。

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

评论


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

–阿蒙
16 Sep 24'6:25

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

–乔治
16-09-24在8:28

#6 楼

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

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

但是,如果所讨论的模块仅由您控制的代码使用,那么您的朋友可能会有一点建议。您可以使用单元测试来验证是否仅使用有效输入调用了该模块。前提条件检查仍然可以被认为是一种很好的做法,但这是一个折衷方案:我向您散布了检查您永远不会出现的条件的代码,这只是掩盖了代码的意图。

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

评论


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

– Peteris
16 Sep 24 '13:55

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

–雅克B
16-09-24在16:22

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

–罗伯特·哈维(Robert Harvey)
16 Sep 24 '16:53

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

–雅克B
16-09-24在17:54

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

– Aleksander Fular
16-9-28在12:37

#7 楼

这种论点让我感到困惑,因为当我开始练习TDD时,我的形式为“对象在时响应<确定方式>”的单元测试增加了2或3倍。我想知道您的同事如何在不进行功能验证的情况下成功通过此类单元测试。相反的情况是,单元测试表明您从未产生过差的输出,无法传递给其他函数的论点则很难证明。与第一种情况一样,它在很大程度上取决于对边缘情况的彻底覆盖,但是您还具有其他要求,即所有功能输入必须来自您已经对其单元进行测试的其他功能的输出,而不是来自用户输入或第三方模块。

换句话说,TDD的作用并不能阻止您需要验证代码,而不能帮助您避免忘记验证代码。

#8 楼

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

在我看来,论点是:


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

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

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

不过请注意,如果在传递值<= 0时记录并测试函数的行为,则负值将不再无效(至少,无效不超过throw的任何自变量,因为也被记录为引发异常!)。呼叫者有权依靠这种防御行为。在语言允许的情况下,这可能是最好的情况-该函数没有“无效输入”,但是希望不引起该函数引发异常的调用者应进行充分的单元测试,以确保它们不会

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

评论


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

– Craig
16-09-25在5:20

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

–史蒂夫·杰索普(Steve Jessop)
16-09-25在16:24

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

– Craig
16-09-25在16:53

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

– Craig
16-09-25在17:02



#9 楼

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

对于任何非私有接口,您的同事“单元测试应该捕获类的任何不正确使用”的主张完全是错误的。如果可以使用整数参数来调用公共函数,则可以并且将使用任何整数参数来调用该公共函数,并且代码应具有适当的行为。如果公共功能签名接受例如Java Double类型,然后为null,NaN,MAX_VALUE和-Inf都是可能的值。您的单元测试无法捕获对该类的不正确使用,因为这些测试无法测试将使用该类的代码,因为该代码尚未编写,您可能未编写并且肯定超出了单元测试的范围。

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

评论


标题和第一段是正确的,因为不是单元测试将在运行时执行代码。它与其他任何运行时代码以及不断变化的实际条件以及错误的用户输入和黑客尝试都将与代码交互。

– Craig
16 Sep 24 '17:43



#10 楼

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

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

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

但是,我怀疑您的同事实际上从某种意义上说是有意义的,在这种情况下,不需要对输入进行特定检查,对不良输入进行特定响应:在这种情况下,仅对鲁棒性有一个一般的了解。

检查进入某些顶级功能的部分原因是,可以保护某些弱的或未经过良好测试的内部代码免受意外组合的影响。参数(这样,如果对代码进行了良好的测试,则不需要进行检查:代码可以“克服”不良的参数)。

同事的想法中有真理,他可能意味着就是这样:如果我们用非常健壮的低级代码构建功能,这些低级别代码经过防御性编码并针对所有滥用情况进行了单独测试,那么高级功能可能很健壮,而无需进行自己的广泛自检。 />
如果违反了合同,它将翻译为som滥用低级功能,可能会引发异常或其他任何错误。

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

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

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

评论


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

–超级猫
16-09-27在2:12

#11 楼

测试定义了您的班级契约。

作为必然的结果,没有测试定义了包含未定义行为的契约。因此,当您将null传递给Foo::Frobnicate(Widget widget),并且发生了难以置信的运行时破坏时,您仍在您的课程中。

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

并且您通过包含一个

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

来记录该决定。

#12 楼

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

这种防御性编程通常会通过完全单元测试来消除

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

#13 楼

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

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

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


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

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

我的观点是,如果仅在内部将代码用作较大应用程序开发的一部分(在TDD下),则TDD断言可以解决负值问题,将成为没有TDD框架和测试,界限检查事项的其他程序员使用的库。

评论


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

– Craig
16 Sep 24'6:04

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

–黑鹰
16-09-26在14:59

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

– Craig
16-09-26在17:13

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

– Craig
16-09-26在17:14



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

– Craig
16-09-26在19:48

#14 楼

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

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

br />现在,如果您只是简单地执行*(sum) = a + b就可以了,但只需要一些输入即可。 a = 1b = 2将成为sum = 3;但是,由于sum的大小为一个字节,因此a = 100b = 200将由于溢出而产生sum = 44。在C语言中,在这种情况下,您将返回错误以表明函数已失败;在代码中抛出异常是相同的。不考虑失败或测试如何处理它们将无法长期运行,因为如果发生这些情况,将无法对其进行处理,并可能导致许多问题。

评论


这看起来像一个很好的采访问题示例(为什么它有一个返回值和一个“出”参数-当sum为空指针时会发生什么?)。

– Toby Speight
18年5月24日在9:22