自动化单元测试与自动化集成测试的优缺点是什么?

时间:2009-04-21 04:15:28

标签: unit-testing automated-tests integration-testing

最近,我们一直在为现有的Java应用程序添加自动化测试。

我们拥有什么

这些测试中的大多数都是集成测试,可能会覆盖一堆调用,如: -

  1. HTTP发布到servlet
  2. servlet验证请求并调用业务层
  3. 业务层通过hibernate等做了很多事情并更新了一些数据库表
  4. servlet生成一些XML,通过XSLT运行它以生成响应HTML。
  5. 然后我们验证servlet是否使用正确的XML进行响应,并且数据库中存在正确的行(我们的开发Oracle实例)。然后删除这些行。

    我们还有一些较小的单元测试来检查单个方法调用。

    这些测试都是我们夜间(或adhoc)构建的一部分。

    问题

    这似乎很好,因为我们正在检查系统的边界:一端是servlet请求/响应,另一端是数据库。如果这些工作正常,那么我们可以自由地重构或混淆其中的任何内容,并确信被测servlet继续工作。

    这种方法可能会遇到什么问题?

    我看不出如何在单个类上添加更多单元测试会有所帮助。难道这不会让重构变得更难,因为我们更有可能需要抛弃并重新编写测试吗?

12 个答案:

答案 0 :(得分:34)

单元测试更严格地定位故障。集成级别测试更符合用户要求,因此可以更好地预测交付成功。除非建造和维护,否则它们都不是很好,但如果使用得当它们都非常有价值。


(更...)

单元测试的一点是,没有集成级别测试可以像一组好的单元测试一样运行所有代码。是的,这可能意味着你必须在某种程度上重构测试,但一般来说你的测试不应该如此依赖于内部。所以,举个例子说,你有一个函数来获得2的幂。你描述它(作为一个正式的方法人,我声称你指定它)

long pow2(int p); // returns 2^p for 0 <= p <= 30

您的测试和您的规范看起来基本相同(这是一种伪xUnit用于说明):

assertEqual(1073741824,pow2(30);
assertEqual(1, pow2(0));
assertException(domainError, pow2(-1));
assertException(domainError, pow2(31));

现在你的实现可以是一个带有倍数的for循环,你可以稍后再把它改成一个班次。

如果您更改实现,以便它返回16位(请记住sizeof(long)仅保证不小于sizeof(short)),那么此测试将很快失败。集成级别的测试可能会失败,但不一定会失败,并且在pow2(28)计算的下游某处不会失败。

重点是他们真的在测试不同的情况。如果你可以构建足够的细节和广泛的集成测试,你可能能够获得相同水平的覆盖率和细粒度测试的程度,但它可能很难做到最好,并且指数状态空间爆炸将打败你。通过使用单元测试对状态空间进行分区,您所需的测试数量会以指数方式增长。

答案 1 :(得分:28)

你要问两种不同的利弊(骑马和骑摩托车的利弊是什么?)

当然两者都是“自动化测试”(〜骑行),但这并不意味着他们可以替代(你不骑马数百英里,而且你不骑摩托车在关闭-vehicle muddy places)


单元测试测试代码的最小单位,通常是一种方法。每个单元测试都与它正在测试的方法密切相关,如果它写得很好,那么它(几乎)仅与之相关。

他们非常适合指导新代码的设计和现有代码的重构。在系统准备进行集成测试之前很久就能发现问题。请注意,我写了指南,所有测试驱动开发都是关于这个词的。

手动单元测试没有任何意义。

重构怎么样,这似乎是你最关心的问题?如果你只重构一个方法的实现(内容),而不是它的存在或“外部行为”,单元测试仍然有效并且非常有用(你无法想象在尝试之前有多大用处)。

如果你更积极地重构,改变方法的存在或行为,那么是的,你需要为每个新方法编写一个新的单元测试,并可能扔掉旧的方法。但编写单元测试,特别是如果你在代码本身之前编写它,将有助于澄清设计(即 方法应该做什么,什么它不应该)不要被实现细节混淆(即 方法应该如何处理它需要做的事情)。


自动集成测试测试代码的最大单元,通常是整个应用程序。

他们非常适合测试您不想手动测试的用例。但是您也可以进行手动集成测试,它们同样有效(只是不太方便)。


今天开始一个新项目,不进行单元测试是没有任何意义的,但我会说,对于像你这样的现有项目来说,为你已经拥有的所有东西编写它们并没有多大意义。工作

在你的情况下,我宁愿用“中间立场”的方法写作:

  1. 较小的集成测试,仅测试您要重构的部分。如果你正在重构整个事情,那么你可以使用当前的集成测试,但是如果你只重构-say- XML生成,那么要求数据库存在没有任何意义,所以我写了一个简单小巧的XML集成测试。
  2. 您要编写的新代码的一系列单元测试。正如我上面已经写过的那样,一旦你“搞乱其中任何东西”,单元测试就会准备就绪,确保你的“混乱”在某个地方。
  3. 事实上,你的集成测试只会确保你的“乱七八糟”不起作用(因为一开始它不起作用,对吧?)但它不会给你任何线索

    • 为什么它不起作用
    • 如果您对“混乱”的调试确实在解决问题
    • 如果您对“混乱”的调试破坏了其他内容

    集成测试只会在整个更改成功时给出确认(并且答案将长期为“否”)。在重构过程中,集成测试不会给你任何帮助,这将使它变得更难并且可能令人沮丧。你需要进行单元测试。

答案 2 :(得分:20)

我同意Charlie关于集成级别测试更多地反映用户操作和整个系统的正确性。我认为单元测试有更多的价值,而不仅仅是更严格地定位故障。单元测试提供了两个主要值,而不是集成测试:

1)编写单元测试与测试一样,都是一种设计行为。如果您练习测试驱动开发/行为驱动开发,编写单元测试的行为可帮助您准确设计代码应该执行的操作。它可以帮助您编写更高质量的代码(因为松散耦合有助于测试)并且它可以帮助您编写足够的代码以使测试通过(因为您的测试实际上符合您的规范)。

2)单元测试的第二个值是,如果它们写得正确,它们的速度非常快。如果我对项目中的某个类进行了更改,我可以运行所有相应的测试来查看是否有任何损坏吗?我如何知道要运行哪些测试?他们需要多长时间?我可以保证它会比编写良好的单元测试更长。您应该能够在几分钟内完成所有单元测试。

答案 3 :(得分:16)

仅举几例个人经验:

单元测试:

  • (+)保持测试接近相关代码
  • (+)相对容易测试所有代码路径
  • (+)很容易看出是否有人无意中改变了方法的行为
  • ( - )为UI组件编写的内容比非GUI
  • 更难

整合测试:

  • (+)在项目中使用螺母和螺栓很好,但集成测试确保它们彼此适合
  • ( - )更难以本地化错误来源
  • ( - )更难以测试所有(甚至所有关键的)代码路径

理想情况下两者都是必要的。

<强>示例:

  • 单元测试:确保输入索引&gt; = 0且&lt;数组的长度。外界时会发生什么?方法应该抛出异常还是返回null?

  • 整合测试:输入负库存值时用户会看到什么?

第二个影响UI和后端。双方都可以完美地工作,你仍然可以得到错误的答案,因为两者之间的错误条件没有明确定义。

我们发现的关于单元测试的最好的部分是,它使开发人员从代码 - &gt; test-&gt;思考到思考 - &gt;测试 - &gt;代码。如果开发人员必须首先编写测试,那么他倾向于更多地考虑前面可能出现的问题。

要回答你的上一个问题,因为单元测试过于接近代码并迫使开发人员更多地预先考虑,实际上我们发现我们不会像往常那样重构代码,所以代码更少被动摇了 - 所以不断抛出和编写新的测试似乎不是问题。

答案 4 :(得分:5)

这个问题肯定有一个哲学的部分,但也指出了务实的考虑。

用作成为更好的开发人员的手段的测试驱动设计有其优点,但并不是必需的。许多优秀的程序员从未编写过单元测试。单元测试的最佳理由是它们在重构时为您提供的强大功能,尤其是当许多人同时更改源时。检查签入中的错误也为项目节省了大量时间(考虑转移到CI模型并建立在checkin而不是nightly上)。因此,如果您编写单元测试,无论是在编写测​​试代码之前还是之后,您都可以确定在那一刻您编写的新代码。以后单元测试可以确保反对该代码会发生什么 - 这可能很重要。单元测试可以在完成QA之前阻止错误,从而加速您的项目。

如果正确完成,集成测试会强调堆栈中元素之间的接口。根据我的经验,集成是项目中最不可预测的部分。让单个部分工作往往不会那么难,但是将所有部分放在一起可能非常困难,因为在这一步可能出现的错误类型。在许多情况下,由于集成中发生的事情,项目迟到了。在此步骤中遇到的一些错误可以在接口中找到,这些接口由于未在另一侧进行通信而在一侧进行了某些更改。集成错误的另一个来源是在开发中发现的配置,但在应用程序进入QA时会被遗忘。集成测试可以帮助显着减少这两种类型。

每种测试类型的重要性都可以争论,但对您来说最重要的是任何一种类型的应用都适用于您的特定情况。有问题的应用程序是由一小群人还是许多不同的团体开发的?您是否有一个存储库用于所有内容,或者每个应用程序的特定组件都有多个存储库?如果你有后者,那么你将面临不同版本的不同组件的相互兼容性的挑战。

每种测试类型都旨在揭示开发阶段不同集成级别的问题,以节省时间。单元测试驱动许多开发人员在一个存储库上运行的输出集成。集成测试(命名不佳)推动了堆栈中组件的集成 - 组件通常由不同的团队编写。集成测试暴露的问题类别通常更耗时。

如此务实,它真的可以归结为您自己的组织/流程中最需要速度的地方。

答案 5 :(得分:3)

区分单元测试和集成测试的是测试运行所需的部件数量。

单元测试(理论上)需要非常(或没有)其他部件才能运行。 集成测试(理论上)需要运行许多(或所有)其他部分。

集成测试测试行为和基础架构。单元测试通常只测试行为。

因此,单元测试适用于测试某些东西,其他东西的集成测试。

那么,为什么单元测试?

例如,在集成测试时很难测试边界条件。示例:后端函数需要正整数或0,前端不允许输入负整数,当您向其传递负整数时,如何确保后端函数的行为正确?也许正确的行为是抛出异常。这对集成测试来说很难。

所以,为此,你需要一个单元测试(函数)。

此外,单元测试有助于消除集成测试期间发现的问题。在上面的示例中,单个HTTP调用有很多失败点:

来自HTTP客户端的调用 servlet验证 从servlet到业务层的调用 业务层验证 数据库读取(休眠) 业务层的数据转换 数据库写(休眠) 数据转换 - &gt; XML XSLT转换 - &gt; HTML HTML的传输 - &gt;客户端

要使集成测试起作用,您需要所有这些过程才能正常工作。对于servlet验证的单元测试,您只需要一个。 servlet验证(可以独立于其他所有内容)。一层中的问题变得更容易追踪。

您需要单元测试和集成测试。

答案 6 :(得分:2)

单元测试执行类中的方法以验证正确的输入/输出,而无需在应用程序的较大上下文中测试类。您可以使用模拟来模拟依赖类 - 您正在对类进行黑盒测试作为独立实体。单元测试应该可以从开发人员工作站运行,无需任何外部服务或软件要求。

集成测试将包括应用程序的其他组件和第三方软件(例如,您的Oracle dev数据库,或者Web应用程序的Selenium测试)。这些测试可能仍然非常快并且作为持续构建的一部分运行,但是因为它们注入了额外的依赖项,它们还冒着注入新错误的风险,这些错误导致 你的代码出现问题但不是由引起的 / em>你的代码。优选地,集成测试也是您注入实际/记录数据并断言整个应用程序堆栈在给定输入的情况下按预期运行的地方。

问题归结为您希望找到什么样的错误以及希望找到它们的速度。单元测试有助于减少“简单”错误的数量,而集成测试可以帮助您找出架构和集成问题,希望模拟Murphy定律对整个应用程序的影响。

答案 7 :(得分:2)

Joel Spolsky撰写了一篇非常有趣的关于单元测试的文章(这是Joel与其他人之间的对话)。

主要想法是单元测试是非常好的,但只有在“有限”数量下使用它们。当100%的代码都在测试用例中时,Joel不建议实现状态。

单元测试的问题在于,当您想要更改应用程序的体系结构时,您必须更改所有相应的单元测试。它需要花费很多时间(甚至可能比重构本身还要多)。在完成所有工作后,只有少数测试会失败。

因此,只针对真正会造成麻烦的代码编写测试。

我如何使用单元测试:我不喜欢TDD所以我首先编写代码然后我测试它(使用控制台或浏览器)只是为了确保这个代码做了nessecary工作。只有在那之后,我才添加“棘手”的测试 - 其中50%的测试在首次测试后失败。

它有效并且不需要太多时间。

答案 8 :(得分:2)

正如许多人所提到的,集成测试会告诉您系统是否正常工作,而单元测试会告诉您它不在哪里。从测试的角度来看,这两种测试相互补充。

  

我看不出如何添加更多   个别班级的单元测试会   救命。这不会让它变得更难   重构因为我们更有可能   将需要扔掉并重写   测试

没有。它将使重构更容易和更好,并使更清楚地看到哪些重构是适当和相关的。这就是为什么我们说TDD是关于设计,而不是关于测试。对于我来说,为一种方法编写一个测试并且弄清楚如何表达该方法的结果应该是如何根据被测试类的其他方法提出一个非常简单的实现,这是很常见的。该实现经常进入被测试的类中。更简单,更可靠的实现,更清晰的边界,更小的方法:TDD - 单元测试,特别是 - 引导您朝着这个方向,而集成测试则没有。它们既重要又有用,但它们有不同的用途。

是的,您可能会发现自己有时会修改和删除单元测试以适应重构;那没关系,但并不难。通过这些单元测试 - 并通过编写它们的经验 - 可以让您更好地了解代码并更好地进行设计。

答案 9 :(得分:1)

您可能也对this question and the related answers感兴趣。你可以找到我在这里已经给出的答案的补充(这是正确的英语吗?:)

答案 10 :(得分:1)

虽然您描述的设置听起来不错,但单元测试也提供了一些重要的功能。单元测试提供了精细的粒度级别。通过松散耦合和依赖注入,您几乎可以测试每个重要的情况。你可以肯定这些单位很健壮;您可以通过在集成测试期间不一定发生的大量输入或有趣的事情来仔细检查各个方法。

E.g。如果你想确定地看到一个类如何处理某种需要棘手设置的故障(例如从服务器检索某些东西时的网络异常),你可以轻松编写自己的测试双网络连接类,注入它并告诉它每当你想要的时候抛出异常。然后,您可以确保正在测试的类正常处理异常并继续处于有效状态。

答案 11 :(得分:1)

我们的项目中有4种不同类型的测试:

  1. 必要时用模拟进行单元测试
  2. DB测试,其行为类似于单元测试,但触摸db&amp;事后清理
  3. 我们的逻辑是通过REST公开的,所以我们有测试做HTTP
  4. 使用WatiN进行Webapp测试,实际使用IE实例并重复主要功能
  5. 我喜欢单元测试。它们运行速度非常快(比#4测试快100-1000倍)。它们是类型安全的,因此重构非常简单(具有良好的IDE)。

    主要问题是需要做多少工作才能正确完成。你必须模仿一切:Db访问,网络访问,其他组件。你必须装饰不可移动的类,获得无数的无用类。你必须使用DI,以便你的组件没有紧密耦合,因此不可测试(请注意,使用DI实际上不是一个缺点:)

    我喜欢测试#2。他们使用数据库并报告数据库错误,约束违规和无效列。我认为我们可以使用它进行有价值的测试。

    <#>#3,尤其是#4更成问题。它们需要构建服务器上的某些生产环境子集。您必须构建,部署并运行应用程序。每次都必须有一个干净的DB。但最终,它还是有回报的。 Watin测试需要不断的工作,但您也可以不断进行测试。我们在每次提交时运行测试,很容易看到我们什么时候破坏。

    所以,回到你的问题。单元测试很快(这非常重要,构建时间应该小于,比如10分钟)并且很容易重构。如果您的设计发生变化,那么比重写整个视频要容易得多。如果您使用带有良好 find usages 命令的精美编辑器(例如IDEA或VS.NET + Resharper),您始终可以找到代码的测试位置。

    使用REST / HTTP测试,您可以很好地验证系统是否真正有效。但测试运行缓慢,因此很难在此级别进行完整的验证。我假设您的方法接受多个参数或可能的XML输入。要检查XML或每个参数中的每个节点,需要几十或几百个调用。你可以用单元测试来做到这一点,但是你不能用REST调用来做到这一点,当每个调用都需要很长时间。

    我们的单元测试比#3测试更频繁地检查特殊边界条件。他们(#3)检查主要功能是否正常,就是这样。这对我们来说似乎很有效。