应该testing内部实现还是只testing公共行为?
鉴于软件在哪里…
- 该系统由几个子系统组成
- 每个子系统由几个组件组成
- 每个组件都使用很多类来实现
…我喜欢编写每个子系统或组件的自动化testing。
我不写一个组件的每个内部类的testing(因为每个类都有助于组件的公共function,因此可以通过组件的公共API从外部testing/testing)。
当我重构组件的实现时(我经常这样做,作为添加新function的一部分),因此我不需要改变任何现有的自动化testing:因为testing只依赖于组件的公共API和公共API通常是扩大而不是改变。
我认为这个政策与像重构testing代码 ( Refactoring Test Code )这样的文档形成了鲜明的对比。
- “…unit testing…”
- “…系统中每个class的testing课程…”
- “…testing代码/生产代码比…理想地认为接近1:1的比率…”
…我想所有这些我都不同意(或者至less不练习)。
我的问题是,如果你不同意我的政策,你会解释为什么吗? 这种testing程度在哪些情况下不足?
综上所述:
- 公共接口被testing(并重新testing),很less发生变化(它们被添加到但很less被修改)
- 内部API隐藏在公共API的后面,可以在不改写testing公共API的testing用例的情况下进行更改
脚注:我的一些“testing用例”实际上是作为数据实现的。 例如,UI的testing用例由包含各种用户input的数据文件和相应的预期系统输出组成。 testing系统意味着具有testing代码,其读取每个数据文件,将input重放到系统中,并声明它获得相应的期望输出。
尽pipe我很less需要更改testing代码(因为公共API通常被添加到而不是被改变),但是我确实发现有时(例如每周两次)需要改变一些现有的数据文件。 这可能会发生在我更改系统输出时(即新function改进了现有输出),这可能会导致现有testing“失败”(因为testing代码只会试图断言输出没有改变)。 为了处理这些情况,我做了以下工作:
- 重新运行自动化testing套件,该testing套件包含一个特殊的运行时间标志,该标志告诉它不声明输出,而是将新的输出捕捉到一个新的目录
- 使用可视化差异工具来查看哪些输出数据文件(即,哪些testing用例)已经改变,并且鉴于新function,validation这些改变是好的并且如预期的那样
- 通过将新目录中的新输出文件复制到运行testing用例的目录(覆盖旧testing)来更新现有testing
脚注:“组件”是指像“一个DLL”或“一个程序集”这样的东西……这个东西足够大,可以在系统的体系结构或部署图上看到,通常使用几十个或100个类来实现,使用一个只包含大约1个或几个接口的公共API …可以分配给一个开发团队(将不同的组件分配给不同的团队)的东西,因此根据Conway定律一个相对稳定的公共API。
脚注:文章面向对象testing:神话与现实说,
神话:黑盒testing就足够了。 如果你使用类接口或规范来做testing用例devise的细致工作,那么你可以放心,这个类已经被充分运用了。 白盒testing(看一个方法的实现来devisetesting)违反了封装的概念。
现实:面向对象的结构问题,第二部分。 许多研究表明,开发人员认为彻底彻底的黑盒testing套件只能在被testing的实现中使用三分之一到一半的语句(更不用说path或状态)。 这有三个原因。 首先,所select的input或状态通常会执行正常path,但不会强制所有可能的path/状态。 其次,黑盒testing本身并不能揭示出惊喜。 假设我们已经testing了被测系统的所有指定行为。 为了确信没有不明确的行为,我们需要知道系统的任何部分是否未被黑盒testing套件行使。 这个信息可以通过代码工具获得。 第三,在不检查源代码的情况下常常难以执行exception和error handling。
我应该补充一点,我正在做白盒functiontesting:我看到了代码(在实现中),我编写了functiontesting(驱动公共API)来执行各种代码分支(function实现的细节)。
我的做法是通过公共API / UI来testing内部。 如果某些内部代码不能从外部访问,那么我就会重新将其删除。
答案很简单:您正在描述functiontesting,这是软件质量保证的重要组成部分。 testing内部实现是unit testing,这是软件质量保证的另一个目标。 这就是为什么你觉得人们不同意你的方法。
functiontesting对于validation系统或子系统是否执行应有的function非常重要。 客户看到的任何东西都应该这样testing。
unit testing是在这里检查你刚才写的10行代码是否应该做什么。 它使你的代码更有信心。
两者都是互补的。 如果你在现有的系统上工作,functiontesting是可能的第一件事。 但是,只要你添加代码,unit testing也是一个好主意。
我没有在我面前的Lakos副本,所以我不会仅仅指出,他会比我更好地解释为什么testing在各个层面都很重要。
只testing“公共行为”的问题就是这样的testing给你提供的信息非常less。 它会捕获许多错误(就像编译器会捕获许多错误),但不能告诉你错误在哪里。 如果一个执行不力的单位很长一段时间会返回好的价值,然后在情况发生变化时停止这样做, 如果该单位直接进行了testing,那么执行不力的事实早就显而易见了。
testing粒度的最佳级别是单位级别。 通过接口为每个单元提供testing。 这使您可以validation和logging您对每个组件的行为方式的信念,这反过来又允许您通过testing引入的新function来testing相关的代码,从而保持testing的短小和目标。 作为奖励,它保持testing他们正在testing的代码。
用不同的方式来表述,只要testing公共行为是正确的,只要你注意到每个公开可见的类都有公共行为。
到目前为止,对这个问题已经有了很多很好的回应,但是我想添加一些我自己的笔记。 作为序言:我是一家为广泛的大客户提供技术解决scheme的大型公司的顾问。 我这样说是因为,根据我的经验,我们被要求比大多数软件商店做得更彻底(除了API开发者)。 以下是我们确保质量的一些步骤:
- 内部unit testing:
开发人员需要为他们编写的所有代码创buildunit testing(阅读:每种方法)。 unit testing应该覆盖积极的testing条件(我的方法是否工作?)和负面的testing条件(当我的一个必需的参数为空时,该方法是否引发ArgumentNullException?)。 我们通常使用CruiseControl.net这样的工具将这些testing整合到构build过程中 - 系统testing/assemblytesting:
有时这个步骤被称为不同的东西,但这是我们开始testing公共function的时候。 一旦你知道你所有的单位单位function如预期,你想知道你的外部function也按照你认为他们应该的方式工作。 这是一种functionvalidation,因为目标是确定整个系统是否按照应有的方式工作。 请注意,这不包括任何积分。 对于系统testing,您应该使用模拟的接口而不是真正的接口,以便您可以控制输出并构buildtesting用例。 - 系统集成testing:
在这个过程的这个阶段,你想把你的集成点连接到系统。 例如,如果您使用的是信用卡处理系统,则您需要在此阶段join实时系统,以validation它仍然有效。 你会想要执行类似的testing系统/组装testing。 - functionvalidationtesting:
functionvalidation是用户通过系统运行或使用API来validation它是否按预期工作。 如果您已经构build了一个开票系统,那么您将在这个阶段执行您的testing脚本,从而确保一切都按照您的devise进行。 这显然是这个过程中的关键阶段,因为它告诉你是否已经完成了你的工作。 - authenticationtesting:
在这里,你把真正的用户放在系统的前面,让他们去看看吧。 理想情况下,您已经在利益相关方的某个位置testing了您的用户界面,但是这个阶段会告诉您目标受众是否喜欢您的产品。 你可能听说过其他厂商称之为“候选版本”的东西。 如果在这个阶段一切顺利的话,你就知道你很乐意投入生产。 authenticationtesting应始终在您将用于生产的相同环境(或至less相同的环境)中执行。
当然,我知道不是每个人都遵循这个过程,但是如果你从头至尾地看,你可以开始看到单个组件的好处。 因为他们发生在不同的时间表上(例如,每天),所以我没有包含构buildvalidationtesting等内容。 我个人认为,unit testing是非常重要的,因为它们让你深入了解你的应用程序的哪个特定组件在哪个具体的用例上失败。 unit testing也可以帮助你隔离哪些方法正常工作,这样你就不用花时间去查看有关失败的更多信息。
当然,unit testing也可能是错误的,但是如果你从function/技术规范开发你的testing用例(你有一个,对吧?)),你不应该有太多的麻烦。
如果您正在从事纯粹的testing驱动开发,那么在您有任何失败的testing后,您只能实现任何代码,并且只有在没有失败的testing时才执行testing代码。 此外,只执行最简单的事情,使失败或通过testing。
在有限的TDD实践中,我已经看到了如何帮助我清除由代码产生的每个逻辑条件的unit testing。 我并不完全相信,我的私有代码的100%的逻辑特性是由我的公共接口公开的。 实践TDD对于这一指标似乎是免费的,但仍然存在公共API所不允许的隐藏function。
我想你可以说这种做法可以保护我免受公共界面中未来的缺陷。 要么你觉得这很有用(并且让你更快地添加新的function),否则你会发现浪费时间。
你可以编写functiontesting; 没关系。 但是,您应该使用testing覆盖率来validation实现,以certificate所testing的代码都具有与functiontesting有关的目的,并且它确实有相关的事情。
你不应该盲目地认为一个单元==一个类。 我认为这可能是反效果的。 当我说我写一个unit testing时,我正在testing一个逻辑单元 – “某事”提供了一些行为。 一个单位可能是一个单一的类,也可能是几个类一起工作来提供这种行为。 有时候,它开始是一个单一的阶级,但后来演变成三,四个阶级。
如果我从一个类开始编写testing,但后来它变成了几个类,我通常不会为其他类编写单独的testing – 它们是被测单元的实现细节。 这样我就可以让我的devise成长,而我的testing也不是那么脆弱。
我曾经认为,这个问题和CrisW一模一样,在更高的层次上进行testing会更好,但在获得更多的经验之后,我的想法会被调整到“每个class级都应该有一个testingclass”。 每个单位都应该进行testing,但是我select定义与以前有所不同的单位。 这可能是CrisW谈到的“组件”,但是它往往也只是一个单一的类。
另外,functiontesting可以足够certificate你的系统能够完成它应该做的事情,但是如果你想用例子/testing(TDD / BDD)驱动你的devise,那么较低的杠杆testing是一个自然的结果。 当你完成实施时,你可以把这些低级别的testing扔掉,但这样做是浪费 – testing是一个积极的副作用。 如果你决定进行剧烈的重构,使你的低级testing无效,那么你把它们扔掉,写一次新的。
分离testing/validation软件的目标,并使用testing/示例来推动您的devise/实现可以澄清这个讨论了很多。
更新:另外,基本上有两种做TDD的方法:外向内和外向外。 BDD促进外部进入,这导致更高级别的testing/规范。 如果你从细节入手,你会为所有课程编写详细的testing。
我同意在这里的大部分post,但是我会添加这个:
testing公共接口主要优先考虑,然后保护,然后是私人。
通常,公共和受保护的接口是私有和受保护接口的组合的总结。
个人情况:你应该testing一切。 给定一个较小的函数的强大的testing集,你会被赋予更高的信心,隐藏的方法工作。 我同意另一个人关于重构的评论。 代码覆盖将帮助您确定代码的额外位,并在必要时重构这些代码。
我个人也testing受保护的部分,因为他们是“公共”的inheritancetypes…
我同意,代码覆盖率理想情况下应该是100%。 这并不一定意味着60行代码将有60行testing代码,但每个执行path都经过testing。 唯一比烦恼更烦人的是一个还没有运行的bug。
通过仅testing公共API,您将会面临不testing内部类的所有实例的风险。 我说的很明显,但我认为应该提到。 每一种行为被testing的越多,不仅认识到它已经破裂,而且认识到破坏的更容易。
我testing私有实现细节以及公共接口。 如果我更改了一个实现细节,而且新版本有一个错误,这使我能够更好地了解错误实际存在的位置,而不仅仅是它的影响。
[对我自己的问题的答案]
也许一个重要的variables是有多less不同的程序员在编码:
-
公理:每个程序员都应该testing自己的代码
-
因此,如果一个程序员编写和交付一个“单元”,那么他们也应该testing这个单元,很可能通过编写一个“unit testing”
-
推论:如果一个程序员编写一个完整的程序包,那么程序员就可以编写整个程序包的functiontesting就足够了(不需要在程序包中编写“单元”unit testing,因为这些单元是其他程序员的实现细节没有直接访问/暴露)。
同样,build立“模拟”组件的做法,你可以testing:
-
如果您有两个团队构build两个组件,那么每个组件都可能需要“模拟”另一个组件,以便在组件被认为已准备好进行后续的“集成testing”之前,他们有一些东西(模拟)来testing他们自己的组件。在另一个团队已经交付他们的组件可以testing你的组件之前。
-
如果您正在开发整个系统,那么您可以发展整个系统…例如,开发一个新的GUI领域,一个新的数据库领域,一个新的商业交易和一个新的系统/functiontesting,所有这些都是一个迭代,而不需要开发任何层的“嘲讽”(因为你可以testing反对真实的东西)。
公理:每个程序员都应该testing自己的代码
我不认为这是普遍的事实。
在密码学方面,有一个众所周知的说法:“创build一个密码很容易,所以你不知道如何自己破解密码。
在你的典型开发过程中,你编写你的代码,然后编译并运行它来检查它是否按你所想的那样工作。 重复这一段时间,你会对代码感到非常自信。
你的自信会使你不那么警惕的testing者。 一个谁不分享你的代码经验不会有问题。
而且,一双新鲜的眼睛可能会有更less的先入之见,不仅仅是代码的可靠性,还有代码的function。 因此,他们可能会拿出代码作者没有想到的testing用例。 人们会期望那些发现更多的错误,或者传播有关代码在组织中做些什么的知识。
另外,还有一个论点是,要成为一名优秀的程序员,你必须担心边缘情况,但是要成为一个好的testing人员,你必须非常担心;-)另外,testing人员可能会更便宜,因此可能需要单独testing团队出于这个原因。
我认为最重要的问题是:哪种方法最适合于发现软件中的错误? 我最近看了一个video(没有链接,对不起),说随机testing比人工生成的testing更便宜,更有效。
这取决于你的devise和最大价值的地方。 一种types的应用程序可能要求另一种types的应用程序。 有时你几乎没有用unit testing来捕捉任何有趣的东西,而function/集成testing则会产生惊喜。 有时unit testing在开发过程中失败了数百次,捕捉到许多错误。
有时它是微不足道的。 一些class级的方式使得testing每一条path的投资回报不那么诱人,所以你可以画一条线,继续敲击一些更重要/更复杂/更重要的东西。
有时仅仅testing公共API是不够的,因为一些特别有趣的逻辑潜藏在内部,而设置系统运行和运行这些特定的path是非常痛苦的。 那是在testing它的胆量时才会有回报。
现在,我倾向于写很多(通常是非常)简单的课,做一两件事。 然后通过将所有复杂的function委托给这些内部类来实现所需的行为。 即我有稍微复杂的交互,但真正简单的类。
如果我改变了我的实现,并且必须重构其中的一些类,我通常不在意。 我尽可能保持我的testing绝缘,所以这往往是一个简单的变化,让他们再次工作。 但是,如果我不得不抛弃一些内部类,我经常replace一些类,并写一些全新的testing。 我经常听到人们抱怨在重构之后不得不保持最新的testing,而且有时不可避免的和令人厌烦的,如果粒度水平足够好,扔掉一些代码+testing通常不是什么大不了的事情。
我觉得这是可testing性devise和不打扰之间的主要区别之一。
你还在遵循这个方法吗? 我也相信这是正确的做法。 你只应该testing公共接口。 现在,公共接口可以是一个服务或一些组件,从某种用户界面或任何其他来源获取input。
但是,您应该能够使用“testing优先”方法演变pupple服务或组件。 即定义一个公共接口并testing它的基本function。 它会失败。 使用后台类API实现这个基本function。 编写API只是为了满足这个firttesting用例。 然后继续询问该服务可以做更多和发展。
只有平衡决策,就是把一个大的服务或组件分解为几个可以重用的小型服务和组件。 如果您坚信一个组件可以跨项目重用。 然后应该为该组件编写自动化testing。 但是,为大服务或组件编写的testing也应该复制已经作为组件testing过的function。
有些人可能会进入理论的讨论,这不是unit testing。 所以这很好。 基本的想法是有testing你的软件的自动化testing。 那么如果它不在单位水平呢。 如果它涵盖与数据库(您控制)的集成,那么它只是更好。
让我知道你是否已经开发出适合你的好的stream程..自从你的第一篇文章
关于ameet