52

给定软件...

  • 该系统由几个子系统组成
  • 每个子系统由几个组件组成
  • 每个组件都使用许多类来实现

...我喜欢为每个子系统或组件编写自动化测试。

我不会为组件的每个内部类编写测试(除非每个类都有助于组件的公共功能,因此可以通过组件的公共 API 从外部进行测试/测试)。

当我重构组件的实现时(我经常这样做,作为添加新功能的一部分),因此我不需要更改任何现有的自动化测试:因为测试只依赖于组件的公共 API 和公共 API通常是扩大而不是改变。

我认为该政策与Refactoring Test Code 之类的文档形成鲜明对比,该文档说...

  • “……单元测试……”
  • “......系统中每个班级的测试班......”
  • “......测试代码/生产代码比率......理想情况下被认为接近1:1的比率......”

...我想我不同意所有这些(或者至少不练习)。

我的问题是,如果您不同意我的政策,您能解释一下原因吗?这种程度的测试在什么场景下是不够的?

总之:

  • 公共接口经过测试(和重新测试),很少更改(它们被添加但很少更改)
  • 内部 API 隐藏在公共 API 之后,无需重写测试公共 API 的测试用例即可更改

脚注:我的一些“测试用例”实际上是作为数据实现的。例如,UI 的测试用例由包含各种用户输入和相应的预期系统输出的数据文件组成。测试系统意味着拥有读取每个数据文件的测试代码,将输入重放到系统中,并断言它获得了相应的预期输出。

虽然我很少需要更改测试代码(因为通常会添加而不是更改公共 API),但我确实发现有时(例如每周两次)需要更改一些现有的数据文件。当我将系统输出更改为更好(即新功能改进现有输出)时,可能会发生这种情况,这可能会导致现有测试“失败”(因为测试代码仅尝试断言输出未更改)。为了处理这些情况,我执行以下操作:

  • 重新运行自动化测试套件,其中有一个特殊的运行时标志,告诉它不要断言输出,而是将新输出捕获到新目录中
  • 使用可视化差异工具查看哪些输出数据文件(即哪些测试用例)已更改,并验证这些更改是否良好且符合新功能的预期
  • 通过将新输出文件从新目录复制到运行测试用例的目录来更新现有测试(覆盖旧测试)

脚注:“组件”是指“一个 DLL”或“一个程序集”之类的东西……大到足以在体系结构或系统部署图中可见的东西,通常使用数十个或 100 个类来实现,并且使用仅由大约 1 个或少数几个接口组成的公共 API……可以分配给一个开发团队的东西(其中不同的组件被分配给不同的团队),因此根据康威定律有一个相对稳定的公共 API。


脚注:文章面向对象测试:神话与现实说,

误区:黑盒测试就足够了。 如果您使用类接口或规范仔细地进行测试用例设计,您可以确信该类已被充分执行。白盒测试(查看方法的实现以设计测试)违反了封装的概念。

现实:OO 结构很重要,第二部分。许多研究表明,被开发人员认为极其彻底的黑盒测试套件仅在被测实现中执行了三分之一到一半的语句(更不用说路径或状态)了。这有三个原因。首先,选择的输入或状态通常使用正常路径,但不会强制所有可能的路径/状态。其次,仅靠黑盒测试无法揭示惊喜。假设我们已经测试了被测系统的所有指定行为。为了确保没有未指定的行为,我们需要知道系统的任何部分是否没有被黑盒测试套件执行过。获取此信息的唯一方法是通过代码检测。第三,

我应该补充一点,我正在做白盒功能测试:我看到代码(在实现中)并编写功能测试(驱动公共 API)来练习各种代码分支(功能实现的细节)。

4

15 回答 15

33

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

功能测试对于验证系统或子系统是否完成了它应该做的事情很重要。客户看到的任何东西都应该以这种方式进行测试。

单元测试用于检查您刚刚编写的 10 行代码是否完成了它应该做的事情。它使您对代码有更高的信心。

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

于 2009-05-13T08:15:48.120 回答
19

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

于 2009-05-13T05:27:46.587 回答
9

我面前没有我的Lakos副本,所以与其引用,我只会指出他在解释为什么测试在各个层面都很重要方面做得比我做得更好。

只测试“公共行为”的问题是这样的测试给你的信息很少。它会捕获许多错误(就像编译器会捕获许多错误一样),但无法告诉您错误在哪里。执行不佳的单元通常会长时间返回良好的值,然后在条件发生变化时停止这样做;如果该单元直接进行了测试,那么它执行不力的事实就会很快显现出来。

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

换一种说法,只测试公共行为是正确的,只要你注意到每个公开可见的类都有公共行为。

于 2009-05-13T05:43:30.350 回答
8

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

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

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

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

于 2009-05-13T18:28:38.683 回答
2

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

在我有限的 TDD 实践中,我已经看到这如何帮助我清除代码产生的每个逻辑条件的单元测试。我不完全相信我的私有代码的逻辑特性 100% 都被我的公共接口公开。实践 TDD 似乎与该指标相辅相成,但仍然存在公共 API 不允许的隐藏功能。

我想你可以说这种做法可以保护我免受公共接口未来缺陷的影响。要么你觉得这很有用(并且可以让你更快地添加新功能),要么你发现这是浪费时间。

于 2009-05-13T05:19:12.527 回答
2

您可以编写功能测试代码;没关系。但是您应该在实现上使用测试覆盖率进行验证,以证明被测试的代码都具有与功能测试相关的目的,并且它实际上做了一些相关的事情。

于 2009-08-29T02:54:47.507 回答
1

你不应该盲目地认为一个单元==一个类。我认为这可能适得其反。当我说我编写单元测试时,我正在测试一个逻辑单元——提供一些行为的“东西”。一个单元可以是单个类,也可以是多个类一起工作以提供该行为。有时它开始是一个单一的班级,但后来演变成三或四个班级。

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

我曾经在这个问题上的想法与 CrisW 的恶魔开始完全一样 - 更高级别的测试会更好,但是在获得更多经验之后,我的想法被缓和到“每个班级都应该有一个测试班”之间。每个单元都应该有测试,但我选择定义的单元与我以前所做的略有不同。它可能是 CrisW 所说的“组件”,但通常它也只是一个类。

此外,功能测试可以很好地证明您的系统完成了它应该做的事情,但是如果您想通过示例/测试 (TDD/BDD) 来推动您的设计,那么较低级别的测试是自然而然的结果。当你完成实现时,你可以扔掉那些低级测试,但这将是一种浪费——测试是一个积极的副作用。如果您决定进行剧烈的重构以使低级测试无效,那么您将它们扔掉并编写一次新的。

将测试/证明您的软件的目标分开,并使用测试/示例来驱动您的设计/实现可以使这个讨论更加清晰。

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

于 2009-08-15T09:42:10.850 回答
1

我同意这里的大多数帖子,但是我要补充一点:

测试公共接口的首要任务是测试,然后是受保护的,然后是私有的。

通常公共和受保护接口是私有和受保护接口的组合的总结。

个人:你应该测试一切。给定一个针对较小功能的强大测试集,您将更有信心相信隐藏的方法有效。我也同意另一个人关于重构的评论。代码覆盖率将帮助您确定额外的代码位在哪里,并在必要时将其重构出来。

于 2009-10-08T23:26:12.510 回答
1

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

但是您应该能够使用 Test First 方法来发展公共服务或组件。即定义一个公共接口并测试它的基本功能。它会失败。使用后台类 API 实现该基本功能。编写 API 以仅满足第一个测试用例。然后继续询问该服务可以做的更多和发展。

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

某些人可能会进行理论讨论,认为这不是单元测试。所以没关系。基本思想是进行自动化测试来测试您的软件。那么,如果它不在单位级别怎么办。如果它涵盖与数据库(由您控制​​)的集成,那么它只会更好。

让我知道您是否开发了任何适合您的良好流程..自从您的第一篇文章..

问候

于 2010-07-27T08:00:58.620 回答
0

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

于 2009-05-13T05:06:27.650 回答
0

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

通过仅测试公共 API,您将面临无法测试内部类的所有实例的风险。我这样说确实是在说明显而易见的事情,但我认为应该提及。对每种行为进行的测试越多,就越容易识别出它不仅被破坏,而且更容易识别出什么被破坏。

于 2009-05-13T05:25:01.787 回答
0

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

于 2009-05-13T05:49:37.173 回答
0

[我自己的问题的答案]

也许很重要的变量之一是有多少不同的程序员在编码:

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

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

  • 推论:如果单个程序员编写了整个包,那么程序员编写整个包的功能测试就足够了(无需编写包内单元的“单元”测试,因为这些单元是其他程序员的实现细节)没有直接访问/曝光)。

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

  • 如果您有两个团队构建两个组件,则每个团队可能都需要“模拟”对方的组件,以便在他们的组件被视为准备好进行后续“集成测试”之前,他们有一些东西(模拟)来测试自己的组件,并且在其他团队交付可以测试您的组件的组件之前。

  • 如果您正在开发整个系统,那么您可以扩展整个系统……例如,开发一个新的 GUI 字段、一个新的数据库字段、一个新的业务事务和一个新的系统/功能测试,所有这些都作为其中的一部分迭代,无需开发任何层的“模拟”(因为您可以改为针对真实事物进行测试)。

于 2009-05-13T18:44:07.860 回答
0

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

我不认为这是普遍正确的。

在密码学中,有句名言:“创建一个密码很容易,安全到你不知道如何自己破解。”

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

你的自信会让你成为一个不那么警惕的测试者。不与您分享代码经验的人不会遇到问题。

此外,一双新的眼睛可能不仅对代码的可靠性,而且对代码的作用有更少的先入之见。结果,他们可能会想出代码作者没有想到的测试用例。人们会期望这些人要么发现更多错误,要么更多地传播有关代码在组织中的作用的知识。

此外,还有一个论点是,要成为一名优秀的程序员,您必须担心边缘情况,但要成为一名优秀的测试人员,您必须过分担心;-) 此外,测试人员可能更便宜,因此值得单独使用出于这个原因,测试团队。

我认为最重要的问题是:哪种方法最适合发现软件中的错误?我最近看了一个视频(没有链接,抱歉),说明随机测试比人工测试更便宜,而且和人工测试一样有效。

于 2009-05-26T03:04:11.647 回答
0

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

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

有时仅仅测试公共 API 是不够的,因为其中潜藏着一些特别有趣的逻辑,并且让系统运行并执行这些特定路径过于痛苦。那是当测试它的胆量时,它确实得到了回报。

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

如果我改变了我的实现并且不得不重构其中的一些类,我通常不在乎。我尽我所能保持我的测试绝缘,所以让它们再次工作通常是一个简单的改变。但是,如果我必须丢弃一些内部类,我通常会替换一些类并编写一些全新的测试。我经常听到人们抱怨在重构之后必须保持测试的最新状态,虽然这有时是不可避免的和令人厌烦的,但如果粒度级别足够精细,那么丢弃一些代码 + 测试通常没什么大不了的。

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

于 2009-10-09T00:05:58.460 回答