自动化单元测试和自动化集成测试有什么优缺点?

29

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

我们现在拥有的

这些测试中大多数是集成测试,可能涵盖一堆调用,例如:

  1. 将HTTP请求提交到servlet中
  2. servlet验证请求并调用业务层
  3. 业务层通过Hibernate等执行一些操作并更新一些数据库表
  4. servlet生成一些XML,通过XSLT运行以生成响应HTML。

然后我们验证servlet是否使用正确的XML进行了响应,并且正确的行存在于数据库中(我们的开发Oracle实例)。 这些行然后被删除。

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

所有这些测试都作为我们每晚(或临时)构建的一部分运行。

问题

这似乎很好,因为我们正在检查系统的边界:一个端点是servlet请求/响应,另一个端点是数据库。 如果这些正常工作,那么我们可以自由地重构或更改两者之间的任何内容,并确信正在测试的servlet仍然有效。

使用这种方法,我们可能会遇到什么问题?

我看不出添加一堆单独类的单元测试如何有助于解决这个问题。那样做不会使重构更加困难,因为我们很可能需要放弃和重新编写测试吗?


从实际情况来看,只要业务逻辑保持简单,你的方法可能是可行的。一旦它变得过于复杂,无法通过集成测试进行适当的测试,问题就会开始出现。当修改业务逻辑时感到紧张时,请尽快引入更多的单元测试。请参阅此相关答案 - Lutz Prechelt
12个回答

35

单元测试可以更紧密地定位失败。集成级别测试更接近用户需求,因此更能预测交付的成功性。 两者都需要建立和维护,如果得当使用,则非常有价值。

关于单元测试的问题是,没有任何一个集成级别测试可以像一组很好的单元测试那样充分运用所有代码。 是的,这可能意味着您必须对测试进行一些重构,但通常您的测试不应太依赖内部结构。 因此,例如假设您有一个获取二次幂的单个函数。 您对其进行描述(作为形式化方法的人,我会声称您对其进行了规定)

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)之后的某个远程下游失败。 重点是它们确实测试不同的情况。如果您可以构建足够详细和广泛的集成测试,则可能能够获得相同级别的覆盖率和精细测试程度,但充其量可能很难做到,并且指数级状态空间爆炸将打败您。通过使用单元测试将状态空间分区,您需要的测试数量增长要少得多。

29
你正在了解两个不同事物的利弊(骑马和骑摩托车的优缺点是什么?)
当然,两者都是“自动化测试”(~骑),但这并不意味着它们是可替代的(你不会骑马数百英里,也不会在不能行驶的泥泞地方骑摩托车)。
单元测试测试代码的最小单元,通常是一个方法。每个单元测试与它测试的方法密切相关,如果编写得好,几乎只与该方法联系在一起。
它们非常适合指导新代码的设计和现有代码的重构。它们非常适合在系统准备进行集成测试之前发现问题。请注意,我写的是指导,而所有的测试驱动开发都是关于这个词的。
手动单元测试毫无意义。
那么重构呢,似乎这是您的主要关注点?如果您只是重构方法的实现(内容),而不是它的存在或“外部行为”,则单元测试仍然有效且非常实用(在尝试之前无法想象有多有用)。
如果您更积极地重构,改变方法的存在或行为,则需要为每个新方法编写新的单元测试,并可能放弃旧的单元测试。但是编写单元测试,特别是在编写代码本身之前编写单元测试,将有助于澄清设计(即方法应该做什么,以及它不应该做什么),而不会被实现细节所困扰(即它应该如何执行需要完成的任务)。
自动集成测试测试代码的最大单元,通常是整个应用程序。
它们非常适合测试您不想手动测试的用例。但是您也可以进行手动集成测试,它们同样有效(只是不那么方便)。

今天开始一个新项目,没有单元测试是没有意义的,但是对于像你这样的现有项目来说,写所有已有并且工作良好的东西的单元测试可能没有太多意义。

在你的情况下,我更倾向于采用一种“折中”的方法:

  1. 编写较小的集成测试,仅测试您要重构的部分。如果您要重构整个项目,则可以使用当前的集成测试,但如果您只重构 - 比如 XML生成部分,那么要求数据库出现就没有任何意义了,所以我会编写一个简单而小巧的XML集成测试。
  2. 针对你要编写的新代码编写一堆单元测试。正如我之前写过的,只要你 “mess with anything in between”,单元测试就会准备好,确保你的“mess”是起作用的。

实际上,您的集成测试只会确保您的“mess”不起作用(因为一开始它不起作用,对吧?),但它不会给您任何关于以下问题的线索:

  • 为什么它不能工作
  • 是否真的正在修复“mess”调试错误
  • “mess”调试是否破坏了其他东西

集成测试只会在整个更改成功后给出确认(相当长的时间答案都是“不”)。集成测试在重构过程中不会提供任何帮助,这将使它更加困难和令人沮丧。你需要单元测试。


3
如果我正在骑马,就不能同时骑摩托车。 - WW.
3
我必须对这个答案中的一些观点提出异议。对于一个“遗留”的项目,您应该朝着全面覆盖的方向努力。您应该致力于进行全面的集成测试,并且如果您通过应用程序的用户界面进行测试,没有任何阻碍。是的,您应该在添加新功能时对其进行单元测试,但是您也应该在更改现有功能时将其置于测试之下。如果您只关注要在测试中加入新代码,那么很多时候您会发现,如果不更改旧代码,则无法进行测试,最终您就会停止测试。任何更改都应该尽可能进行单元测试。 - Edward Strange

20

我同意查理的观点,即集成测试与用户操作和系统整体的正确性更相符。但我认为单元测试比仅仅更紧密地定位故障具有更多价值。单元测试提供了两个主要价值:

1)编写单元测试与测试一样是设计的行为。如果您实践测试驱动开发/行为驱动开发,编写单元测试的行为帮助您确切设计您的代码应该做什么。这有助于您编写更高质量的代码(因为松散耦合有助于测试),并且它可以帮助您编写足够使测试通过的代码(因为实际上您的测试就是您的规范)。

2)单元测试的第二个价值在于,如果它们被正确编写,那么它们非常快。如果我更改项目中的一个类,我能运行所有相关的测试来查看是否出现故障吗?我如何知道要运行哪些测试?需要多长时间?我敢保证它将比编写良好的单元测试要长。你应该能够在最多几分钟内运行所有单元测试。


我也是,但问题是什么区别它们。 - Charlie Martin
没错,我觉得这样做更能突显出它们的独特之处,而不仅仅是将失败本地化。 - James Avery
请注意,我们正在修改现有的应用程序。我们通常会向大部分现有代码添加功能并进行错误修复。目前,我们的集成测试需要大约9分钟才能运行。它们每晚都会运行。 - WW.
6
夜间测试很好,但是良好的单元测试可以在不到一分钟的时间内运行。您可以在编写代码时进行多次运行,在检入之前、检出之后等时刻运行它们。在调试错误时,单元测试也非常有用,您可以编写一个测试来证明错误存在,然后修复它,通过工作测试证明已经修复,并确保它不会再次发生。请注意,本人会尽力保持原意和易读性。 - James Avery

16

个人经验中的一些例子:

单元测试:

  • (+) 使测试与相关代码紧密相连
  • (+) 相对容易测试所有代码路径
  • (+) 容易发现某人无意间更改了方法的行为
  • (-) 比非GUI组件更难编写 UI 组件的单元测试

集成测试:

  • (+) 在项目中拥有螺丝和螺母很好,但集成测试可以确保它们彼此匹配
  • (-) 更难定位错误源
  • (-) 更难测试所有(甚至是所有必要的)代码路径

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

例子:

  • 单元测试:确保输入索引>= 0且<数组长度。当超出边界时会发生什么?方法应该抛出异常还是返回 null?

  • 集成测试:当输入负库存值时,用户会看到什么?

第二个影响UI和后端。两侧都可以完美运行,但由于两者之间的错误条件没有定义明确,所以您仍然可能得到错误的答案。

我们发现单元测试最好的部分是它使开发人员从代码 -> 测试 -> 思考,变为思考 -> 测试 -> 代码。如果开发人员必须首先编写测试,他们倾向于提前考虑可能出现的问题。

回答您的最后一个问题,由于单元测试与代码紧密相连并迫使开发人员提前进行更多思考,在实践中我们发现不需要经常重构代码,所以不断丢弃并编写新测试似乎不是问题。


5
这个问题肯定有哲学的部分,但也涉及实用的考虑。
以测试驱动设计为手段成为更好的开发者是有优点的,但并非必须的。许多优秀的程序员从未编写单元测试。编写单元测试的最佳原因是它们在重构时给你的能力,特别是当许多人同时更改源代码时。在提交时检测错误也可以节省项目的大量时间(考虑采用CI模型,在提交时进行构建而不是每晚)。因此,如果您编写单元测试,无论是在编写测试代码之前还是之后,您都可以确定新编写的代码。单元测试确保了代码稍后可能发生的情况 - 这可能是重要的。单元测试可以在错误到达QA之前停止错误,从而加快项目进程。
集成测试正确执行时会强调堆栈中各个元素之间的接口。根据我的经验,集成是项目中最不可预测的部分。使各个部分工作通常并不难,但由于此步骤可能出现的错误类型,将所有东西放在一起可能非常困难。在许多情况下,项目因集成中发生的事情而延迟。在此步骤中遇到的一些错误是在某一侧进行的某些更改破坏的接口中发现的。集成错误的另一个来源是在dev中发现但在应用程序进入QA时被遗忘的配置。集成测试可以帮助大大减少这两种类型的错误。
每种测试类型的重要性可能会有争议,但对您最重要的是将任一类型应用于您特定的情况。所讨论的应用程序是由一小组人还是许多不同的组开发的?您是否为所有内容拥有一个存储库,还是为应用程序的每个组件单独拥有多个存储库?如果您有后者,则会面临不同版本的不同组件的互操作性挑战。
每种测试类型都旨在暴露开发阶段不同级别集成的问题以节省时间。单元测试驱动多个开发人员在一个存储库上的输出集成。集成测试(名称不当)驱动堆栈中组件的集成 - 这些组件通常由不同的团队编写。集成测试公开的问题类通常需要更多时间来修复。
因此,实际上,它真正取决于您在自己的org / process中最需要速度的地方。

3
区分单元测试和集成测试的关键在于测试运行所需的部件数量。
理论上,单元测试只需要非常少(或没有)其他部件即可运行。 理论上,集成测试需要许多(或全部)其他部件才能运行。
集成测试可以测试行为和基础设施,而单元测试通常只测试行为。
因此,单元测试适合测试某些内容,而集成测试适合测试其他内容。
那么,为什么要进行单元测试?
例如,当进行集成测试时,很难测试边界条件。例如:后端函数期望正整数或0,前端不允许输入负整数,如何确保将负整数传递给它时,后端函数的行为正确?也许正确的行为是抛出异常。这在集成测试中非常难做到。
因此,您需要一个单元测试(针对该函数)。
此外,单元测试有助于消除在集成测试中发现的问题。在上面的例子中,单个HTTP调用存在许多故障点:
来自HTTP客户端的调用 Servlet验证 Servlet到业务层的调用 业务层验证 数据库读取(Hibernate) 业务层数据转换 数据库写入(Hibernate) 数据转换-> XML XSLT转换-> HTML HTML->客户端的传输
为使集成测试正常工作,您需要所有这些过程都正常工作。对于Servlet验证的单元测试,您只需要一个。 Servlet验证(可以独立于其他所有内容)。一层中的问题变得更容易跟踪。
您需要进行单元测试和集成测试。

2

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

集成测试将包括您的应用程序和第三方软件的其他组件(例如您的Oracle dev数据库,或者用于Web应用程序的Selenium测试)。这些测试可能仍然非常快速,并且作为连续构建的一部分运行,但是由于它们注入了额外的依赖项,因此它们也有风险注入新的错误,导致为您的代码带来问题,但并非由您的代码引起。最好的情况下,集成测试是您注入真实/记录数据并断言整个应用程序堆栈在给定这些输入的情况下表现正常的地方。

问题归结为您要查找哪种类型的错误以及您希望多快地找到它们。单元测试有助于减少“简单”错误的数量,而集成测试则帮助您查找架构和集成问题,希望模拟Murphy法则对整个应用程序的影响。


2
乔尔·斯波尔斯基写了一篇非常有趣的关于单元测试的文章(这是他和其他人之间的对话)。
主要想法是单元测试是非常好的东西,但只有在“有限”的数量下使用才有效。乔尔不建议达到100%的代码都在测试用例下的状态。
单元测试的问题在于,当您想要更改应用程序的架构时,您将不得不更改所有相应的单元测试。这将需要很多时间(甚至可能比重构本身还要花费更多的时间)。而且经过所有这些工作后,只有几个测试会失败。
因此,只为真正可能引起麻烦的代码编写测试。
我如何使用单元测试:我不喜欢TDD,所以我先编写代码,然后我测试它(使用控制台或浏览器),只是为了确保这段代码执行必要的工作。只有在那之后,我才添加“棘手”的测试-其中50%的测试在第一次测试后失败。
这很有效,也不需要太多时间。

不确定为什么有人给你点了踩...听起来是个好建议 - Andrew

2

2
我们的项目中有4种不同类型的测试:
  1. 使用模拟进行单元测试
  2. DB测试类似于单元测试,但会涉及数据库并在测试后清除数据
  3. 我们通过REST公开了逻辑,因此我们有进行HTTP测试的方法
  4. 使用WatiN进行Web应用程序测试,实际使用IE实例并检查主要功能
我喜欢单元测试。它们运行速度非常快(比#4测试快100-1000倍)。它们是类型安全的,所以重构相当容易(使用好的IDE)。
主要问题是需要花费多少工作才能正确地完成它们。您必须模拟所有内容:Db访问,网络访问,其他组件。您必须装饰无法模拟的类,获得成千上万个大多数无用的类。您必须使用DI,以便您的组件没有紧密耦合,因此不可测试(请注意,实际上使用DI并不是缺点:)
我喜欢#2测试。它们确实使用数据库,并将报告数据库错误,约束违规和无效列。我认为我们可以通过这种方式获得有价值的测试。
#3和特别是#4更有问题。它们需要在构建服务器上构建生产环境的某些子集。您必须构建,部署并运行应用程序。您每次都必须有一个干净的数据库。但最终会得到回报。Watin测试需要不断地工作,但您也会获得不断的测试。我们在每次提交时运行测试,并且很容易看到我们是否出了问题。
因此,回到您的问题。单元测试速度快(这非常重要,构建时间应小于10分钟),易于重构。比重新编写整个watin更容易。如果您使用具有良好“查找用法”命令的漂亮编辑器(例如IDEA或VS.NET + Resharper),则始终可以找到测试代码所在的位置。
通过REST / HTTP测试,您可以获得对系统实际工作的良好验证。但是测试运行速度较慢,因此很难在这个级别上进行完全验证。我假设您的方法接受多个参数或可能的XML输入。要检查XML中的每个节点或每个参数,需要进行数十个或数百个调用。您可以使用单元测试进行此操作,但无法使用REST调用进行此操作,因为每个调用可能占用大部分秒数。
我们的单元测试通常会检查特殊边界条件,而#3测试则只检查主要功能是否正常。这对我们似乎运行得相当不错。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接