应该测试内部实现,还是仅测试公共行为?

时间:2009-05-13 04:52:49

标签: unit-testing refactoring automated-tests integration-testing code-coverage

15 个答案:

答案 0 :(得分:31)

答案非常简单:您正在描述功能测试,这是软件质量保证的重要组成部分。测试内部实现是单元测试,这是具有不同目标的软件QA的另一部分。这就是为什么你觉得人们不同意你的方法。

功能测试对于验证系统或子系统是否按预期执行操作非常重要。客户看到的任何东西都应该以这种方式进行测试。

单元测试用于检查您刚编写的10行代码是否符合预期要求。它使您对代码更有信心。

两者都是互补的。如果您在现有系统上工作,可能首先要进行功能测试。但是只要你添加代码,单元测试也是一个好主意。

答案 1 :(得分:17)

我的做法是通过公共API / UI测试内部。如果无法从外部访问某些内部代码,那么我会重构以删除它。

答案 2 :(得分:9)

我没有Lakos的副本在我面前,所以我不会仅仅指出他做得比我解释为什么测试在各个层面都很重要。 / p>

仅测试“公共行为”的问题是这样的测试给你的信息很少。它会捕获许多错误(就像编译器会捕获许多错误一样),但不能告诉你错误在哪里。一个执行不良的单位长时间返回好的值然后在条件改变时停止这样做是很常见的;如果该单位已被直接测试,那么它实施得很糟糕的事实会更早显现出来。

最佳级别的测试粒度是单位级别。通过其接口为每个单元提供测试。这允许您验证和记录您对每个组件行为的看法,这反过来允许您通过仅测试它引入的新功能来测试相关代码,从而使测试保持简短和目标。作为奖励,它会使用他们正在测试的代码进行测试。

换句话说,只测试公共行为是正确的,只要您注意到每个公开可见的类都有公共行为。

答案 3 :(得分:8)

到目前为止,对这个问题有很多很好的回答,但我想补充一些我自己的笔记。作为序言:我是一家大公司的顾问,为大量客户提供技术解决方案。我这样说是因为根据我的经验,我们需要比大多数软件商店更彻底地测试(除了API开发人员)。以下是我们为确保质量而采取的一些步骤:

  • 内部单元测试:
    期望开发人员为他们编写的所有代码创建单元测试(读取:每个方法)。单元测试应该包括正测试条件(我的方法是否工作?)和负测试条件(当我的一个必需参数为null时,该方法是否抛出ArgumentNullException?)。我们通常使用CruiseControl.net等工具将这些测试合并到构建过程中
  • 系统测试/组装测试:
    有时这个步骤被称为不同的东西,但这是我们开始测试公共功能的时候。一旦您知道所有单个单元按预期运行,您就会知道您的外部功能也以您认为应该的方式工作。这是功能验证的一种形式,因为目标是确定整个系统是否以其应有的方式工作。请注意,这不包括任何集成点。对于系统测试,您应该使用模拟接口而不是真实接口,以便您可以控制输出并围绕它构建测试用例。
  • 系统集成测试:
    在此过程的这个阶段,您希望将集成点连接到系统。例如,如果您使用的是信用卡处理系统,则需要在此阶段合并实时系统以验证它是否仍然有效。您可能希望对系统/装配测试执行类似的测试。
  • 功能验证测试:
    功能验证是指在系统中运行或使用API​​验证其是否按预期工作的用户。如果您已经构建了一个发票系统,那么您将在这个阶段从头到尾执行测试脚本,以确保一切都按照您的设计运行。这显然是这个过程中的一个关键阶段,因为它告诉你你是否已经完成了自己的工作。
  • 认证测试:
    在这里,您将真实用户放在系统前面,然后让他们自己动手。理想情况下,您已经在某些时候与利益相关者测试了您的用户界面,但此阶段将告诉您目标受众是否喜欢您的产品。您可能已经听说过其他供应商称之为“发布候选人”。如果在这个阶段一切顺利,你知道你很有可能进入生产阶段。认证测试应始终在您将用于生产的相同环境中执行(或至少在相同的环境中)。

当然,我知道不是每个人都遵循这个过程,但如果你从头到尾看,它可以开始看到各个组件的好处。我没有包括构建验证测试之类的东西,因为它们发生在不同的时间线上(例如,每天)。我个人认为单元测试至关重要,因为它们可以让您深入了解应用程序的哪个特定组件在哪个特定用例中失败。单元测试还可以帮助您确定哪些方法正常运行,这样您就不会花时间查看它们,以获取有关故障的更多信息。

当然,单元测试也可能是错误的,但是如果你根据功能/技术规范开发测试用例(你有一个,对吧?;)),你不应该有太多麻烦。

答案 4 :(得分:2)

如果您正在练习纯测试驱动开发,那么您只有在进行任何失败测试后才能实现任何代码,并且只有在没有失败测试时才实现测试代码。另外,只实现最简单的事情来进行失败或通过测试。

在有限的TDD实践中,我已经看到了这有助于我为代码产生的每个逻辑条件清除单元测试。我并不完全相信我的私有代码的100%逻辑功能都是由我的公共接口公开的。实践TDD似乎是该指标的补充,但仍然存在公共API不允许的隐藏功能。

我想你可以说这种做法可以保护我免受公共界面中未来的缺陷。你发现它很有用(并且可以让你更快地添加新功能),或者你发现这是浪费时间。

答案 5 :(得分:2)

您可以编写功能测试代码;没关系。但是你应该使用测试覆盖率来验证实现,以证明所测试的代码都具有相对于功能测试的目的,并且它实际上做了相关的事情。

答案 6 :(得分:1)

你不应该盲目地认为一个单位==一个班级。我认为这可能会适得其反。当我说我写一个单元测试时,我正在测试一个逻辑单元 - “某事”提供了一些行为。单元可以是单个类,也可以是多个类一起工作以提供该行为。有时它最初只是一个单独的类,但后来演变成三到四个类。

如果我从一个类开始并为此编写测试,但后来它变成了几个类,我通常不会为其他类编写单独的测试 - 它们是被测单元中的实现细节。这样我就可以让我的设计成长,而且我的测试也不那么脆弱。

我过去认为这个问题与CrisW示范完全相同 - 在更高级别进行测试会更好,但在获得更多经验之后,我的思想会被缓和到“每个班级应该有一个测试课程”之间。每个单位都应该有测试,但我选择定义我的单位与我曾经做过的略有不同。它可能是CrisW谈论的“组件”,但通常它也只是一个单独的类。

此外,功能测试可以很好地证明您的系统能够完成它应该做的事情,但是如果您想通过示例/测试(TDD / BDD)来驱动您的设计,则较低的杠杆测试是自然的结果。当你完成实施时,你可以抛弃那些低级别的测试,但那将是一种浪费 - 测试是一个积极的副作用。如果你决定做大幅度的重构使你的低级测试无效,那么你就把它们抛弃并写一次新的。

分离测试/证明您的软件的目标,并使用测试/示例来推动您的设计/实施可以澄清这个讨论。

更新:此外,基本上有两种方式进行TDD:从外到内和外向内。 BDD促进从外到内,这导致更高级别的测试/规范。但是,如果从详细信息开始,您将为所有类编写详细的测试。

答案 7 :(得分:1)

我同意这里的大部分帖子,不过我会补充一下:

测试公共接口有一个主要优先级,然后是受保护的,然后是私有的。

通常,公共接口和受保护接口是私有接口和受保护接口组合的摘要。

个人:你应该测试一切。鉴于针对较小函数的强大测试集,您将更有信心隐藏方法的工作原理。我同意另一个人关于重构的评论。代码覆盖率将帮助您确定代码的额外位置,并在必要时重构这些代码。

答案 8 :(得分:1)

你还在遵循这种方法吗? 我也相信这是正确的方法。 您应该只测试公共接口。 现在,公共接口可以是一个服务或一些从某种UI或任何其他来源获取输入的组件。

但您应该能够使用Test First方法改进puplic服务或组件。即定义一个公共接口并测试它的基本功能。它会失败。 使用后台类API实现该基本功能。编写API只满足这个测试用例。然后继续询问服务可以做些什么并发展。

只有平衡应该采取的决定是将一个大服务或组件分解为几个可以重用的小服务和组件。如果您坚信某个组件可以在项目中重复使用。然后应该为该组件编写自动化测试。但是,为大型服务或组件编写的测试应该再次复制已经作为组件测试的功能。

某些人可能会进入理论上讨论这不是单元测试。那很好。基本思想是进行自动测试以测试您的软件。那么如果它不是单位级别呢?如果它涵盖了与数据库(你控制的)的集成,那么它只会更好。

如果您已经开发出适合您的任何优秀流程,请告知我们......自您的第一篇文章以来......

问候 ameet

答案 9 :(得分:0)

我个人也测试受保护的部分,因为它们对于继承的类型是“公开的”......

答案 10 :(得分:0)

我同意代码覆盖率理想情况下应为100%。这并不一定意味着60行代码将具有60行测试代码,但是每个执行路径都经过测试。唯一比bug还烦人的是一个尚未运行的bug。

通过仅测试公共API,您将面临不测试内部类的所有实例的风险。我真的说明了这一点,但我认为应该提到。每种行为的测试越多,就越容易识别出它被破坏了,但是它被破坏了。

答案 11 :(得分:0)

我测试私有实现细节以及公共接口。如果我更改实现细节并且新版本有错误,这可以让我更好地了解错误的实际位置,而不仅仅是它的影响。

答案 12 :(得分:0)

[回答我自己的问题]

可能其中一个重要的变量是有多少不同的程序员编码:

  • Axiom:每个程序员都应该测试自己的代码

  • 因此:如果程序员编写并发送一个“单元”,那么他们也应该测试该单元,很可能是通过编写“单元测试”

  • 推论:如果一个程序员编写了一个完整的程序包,那么编程人员就可以编写整个程序包的功能测试(不需要在程序包中编写单元“单元”测试,因为这些单元是其他程序员无法直接访问/曝光的实现细节。

同样,构建可以测试的“模拟”组件的做法:

  • 如果你有两个团队构建两个组件,每个组件可能需要“模拟”另一个组件,以便他们有一些东西(模拟)来测试他们自己的组件,然后他们的组件被认为可以用于后续“集成测试”,在其他团队交付了可以测试组件的组件之前。

  • 如果您正在开发整个系统,那么您可以扩展整个系统...例如,开发新的GUI字段,新的数据库字段,新的业务事务和一个新的系统/功能测试,作为一次迭代的一部分,不需要开发任何层的“模拟”(因为你可以测试真实的东西)。

答案 13 :(得分:0)

  

Axiom:每个程序员都应该测试自己的代码

我不认为这是普遍真实的。

在密码学中,有一句众所周知的说法:“创建一个密码很容易,你不知道如何自己破解密码。”

在典型的开发过程中,您编写代码,然后编译并运行它以检查它是否按照您的想法执行。重复这一段时间,你会对你的代码充满信心。

你的信心会使你成为一个不太警惕的测试者。不与您分享您的代码经验的人不会有这个问题。

此外,一双新鲜的眼睛可能会有更少的先入之见,不仅仅是代码的可靠性,还有代码所做的事情。因此,他们可能会提出代码作者没有想到的测试用例。人们可能希望那些人能够发现更多的错误,或者传播关于代码在组织周围做些什么的知识。

此外,还有一个论点是成为一名优秀的程序员,你必须担心边缘情况,但要成为一个好的测试人员,你有痴迷;-)同样,测试人员可能会更便宜,所以它可能是值得的因此而拥有一个单独的测试团队。

我认为最重要的问题是:哪种方法最适合发现软件中的错误?我最近观看了一段视频(没有链接,抱歉),声称随机测试比人类生成的测试更便宜,效果更好。

答案 14 :(得分:0)

这取决于您的设计以及最大价值所在。一种类型的应用可能需要与另一种应用不同的方法。有时你几乎没有抓住任何有趣的单元测试,而功能/集成测试会产生惊喜。有时单元测试在开发过程中会失败数百次,从而捕获许多错误。

有时它是微不足道的。一些课程挂在一起的方式使得测试每条路径的投资回报不那么诱人,所以你可能只是画一条线并继续锤击更重要/更复杂/大量使用的东西。

有时仅仅测试公共API是不够的,因为一些特别有趣的逻辑潜伏在内部,并且设置系统运行并运用这些特定路径是非常痛苦的。那时测试它的内核确实有回报。

现在,我倾向于编写许多(通常极其)简单的类,这些类可以做一两件事。然后,我通过将所有复杂功能委托给那些内部类来实现所需的行为。即我有一些稍微复杂的交互,但是非常简单的类。

如果我改变我的实现并且必须重构其中一些类,我通常不在乎。我尽可能保持我的测试绝缘,所以通常只需要一个简单的改变让他们再次工作。但是,如果我必须抛弃一些内部类,我经常会替换少数类并编写一些全新的测试。我经常听到人们抱怨在重构之后必须保持测试更新,虽然它有时是不可避免和令人厌烦的,但如果粒度级别足够好,那么扔掉一些代码+测试通常不是什么大问题。

我觉得这是设计可测性而不是打扰之间的主要区别之一。