集成和单元测试有什么区别?

What is the difference between integration and unit tests?

我知道所谓的教科书中关于单元测试和集成测试的定义。我好奇的是什么时候该写单元测试了…我将写它们来覆盖尽可能多的类集。

例如,如果我有一个Word类,我将为Word类编写一些单元测试。然后,我开始编写我的Sentence类,当它需要与Word类交互时,我经常编写单元测试,以便它们同时测试SentenceWord…至少在他们互动的地方。

这些测试本质上已经成为集成测试了吗,因为它们现在测试了这两个类的集成,还是仅仅是跨越两个类的单元测试?

一般来说,由于这条不确定的线,我很少真正地写集成测试…或者,我是否使用成品来查看所有的部分是否都能正常地工作在实际的集成测试中,即使它们是手动的,并且很少在每个单独的特性范围之外重复?

我是否误解了集成测试,或者集成测试和单元测试之间的差别真的很小?

编辑

感谢大家的精彩回应!我认为,从各种各样的答案中可以清楚地看出,单元测试和集成测试之间的界限肯定是一条模糊的界限,也许尝试找出哪些是界限,真正的重点应该放在代码上有点幼稚(谢谢@rob cooper)。另外,很抱歉,我不接受任何答案,因为太多太好了,这看起来很主观。


对我来说,关键的区别在于集成测试揭示了一个特性是工作的还是被破坏的,因为它们在一个接近现实的场景中强调了代码。它们调用一个或多个软件方法或特性,并测试它们是否按预期工作。

相反,测试单个方法的单元测试依赖于(通常是错误的)假设,即软件的其余部分正常工作,因为它显式地模拟每个依赖。

因此,当实现某些特性的方法的单元测试为绿色时,并不意味着该特性正在工作。

假设你有这样的方法:

1
2
3
4
5
public SomeResults DoSomething(someInput) {
  var someResult = [Do your job with someInput];
  Log.TrackTheFactYouDidYourJob();
  return someResults;
}

DoSomething对您的客户非常重要:它是一个功能,唯一重要的东西。这就是为什么您通常编写一个黄瓜规范来断言它:您希望验证和通信该特性是否有效。

1
2
3
4
5
6
7
8
9
Feature: To be able to do something
  In order to do something
  As someone
  I want the system to do this thing

Scenario: A sample one
  Given this situation
  When I do something
  Then what I get is what I was expecting for

毫无疑问:如果测试通过,您可以断言您正在交付一个工作特性。这就是你所说的商业价值。

如果您想为DoSomething编写一个单元测试,您应该假装(使用一些模拟)其余的类和方法都在工作(即:方法使用的所有依赖项都正常工作),并断言您的方法正在工作。

在实践中,你会做如下的事情:

1
2
3
4
5
public SomeResults DoSomething(someInput) {
  var someResult = [Do your job with someInput];
  FakeAlwaysWorkingLog.TrackTheFactYouDidYourJob(); // Using a mock Log
  return someResults;
}

您可以使用依赖项注入、工厂方法或任何模拟框架或只是扩展测试中的类来实现这一点。

假设Log.DoSomething()中有一个bug。幸运的是,gherkin规范会找到它,您的端到端测试将失败。

这项功能不起作用,因为Log坏了,而不是因为[Do your job with someInput]不起作用。顺便说一下,[Do your job with someInput]是这种方法的唯一责任。

另外,假设Log在100个其他特性中使用,在100个其他类的100个其他方法中使用。

是的,100个功能将失败。但是,幸运的是,100个端到端的测试也失败了,并且暴露了这个问题。是的,他们说的是实话。

这是非常有用的信息:我知道我的产品坏了。这也是非常令人困惑的信息:它没有告诉我问题在哪里。它告诉我症状,而不是根本原因。

然而,DoSomething的单元测试是绿色的,因为它使用的是一个假的Log,它的构建是为了永不中断。而且,是的:这显然是在撒谎。它正在通信一个坏特性正在工作。它如何有用?

(如果DoSomething()的单元测试失败,请确定:[Do your job with someInput]有一些错误。)

假设这是一个具有中断类的系统:A system with a broken class

一个bug会破坏几个特性,几个集成测试也会失败。

A single bug will break several features, and several integration tests will fail

另一方面,同一个bug只会破坏一个单元测试。

The same bug will break just one unit test

现在,比较两种情况。

同样的错误只会破坏一个单元测试。

  • 使用损坏的Log的所有功能都是红色的
  • 您的所有单元测试都是绿色的,只有Log的单元测试是红色的

实际上,使用中断特性的所有模块的单元测试都是绿色的,因为通过使用mock,它们删除了依赖项。换句话说,他们在一个理想的,完全虚构的世界里运行。这是隔离和查找错误的唯一方法。单元测试意味着模拟。如果你不是在模仿,你就不是在单元测试。

差异

集成测试告诉我们什么不起作用。但在猜测问题可能在哪里时,它们是没有用的。

单元测试是唯一能告诉您错误确切位置的测试。为了获取这些信息,他们必须在模拟环境中运行该方法,在这种环境中,所有其他依赖项都应该正确工作。

这就是为什么我认为你的句子"或者它只是一个跨越两个类的单元测试"被某种方式取代的原因。单元测试不应该跨越2个类。

这个回答基本上是我在这里写的内容的总结:单元测试是谎言,这就是我喜欢它们的原因。


当我编写单元测试时,我通过模拟依赖项将要测试的代码的范围限制在我当前正在编写的类中。如果我正在写一个句子类,而句子对单词有依赖性,我会用一个模拟词。通过模仿单词,我只能集中在它的接口上,当它与单词的接口交互时,我可以测试我的句子类的各种行为。这样我只测试句子的行为和实现,而不同时测试单词的实现。

一旦我编写了单元测试来确保句子在与基于Word接口的Word交互时的行为正确,那么我就编写了集成测试来确保我对交互的假设是正确的。为此,我提供了实际的对象,并编写了一个测试,测试的功能最终将同时使用句子和单词。


我的10位:D

我总是被告知单元测试是对单个组件的测试——应该充分地进行测试。现在,这往往有很多层次,因为大多数组件是由更小的部件组成的。对我来说,一个单元是系统的一个功能部分。因此,它必须提供一些有价值的东西(即,不是用于字符串分析的方法,可能是一个htmlsanitizer)。

集成测试是下一步,它将采用一个或多个组件,并确保它们按应该的方式协同工作。然后,您将不再担心组件如何单独工作,但当您将HTML输入到HTMLeditControl中时,不知何故,它神奇地知道它是否有效。

不过,这是一条真正可以移动的线……我宁愿把精力更多地放在让该死的代码完全停止工作上。^^


单元测试使用模拟

您所说的是实际测试整个系统集成的集成测试。但是,当您进行单元测试时,实际上应该分别测试每个单元。其他的一切都应该被嘲笑。所以在你的例子中,如果使用Word类,那么你的Word类应该被嘲笑。这样,您将只测试Sentence类的功能。


我认为当您开始考虑集成测试时,您所说的更多的是物理层之间的交叉,而不是逻辑层。

例如,如果您的测试关注于生成内容,那么它是一个单元测试:如果您的测试关注于只向磁盘写入内容,那么它仍然是一个单元测试,但是一旦您同时测试了I/O和文件的内容,那么您就有了一个集成测试。当您在服务中测试一个函数的输出时,它是一个单元测试,但是一旦您进行一个服务调用并查看函数结果是否相同,那么这就是一个集成测试。

从技术上讲,你不能只对一个类进行单元测试。如果你的班级是由其他几个班级组成的呢?这会自动使它成为一个集成测试吗?我不这么认为。


采用单一责任设计,其黑白相间。超过1个职责,这是一个集成测试。

通过鸭子测试(看,嘎嘎叫,摇曳,它是一只鸭子),它只是一个单元测试,里面有一个以上的新物体。

当您进入MVC并测试它时,控制器测试总是集成的,因为控制器同时包含一个模型单元和一个视图单元。在那个模型中测试逻辑,我将调用单元测试。


你考试的性质

模块X的单元测试是一种只在模块X中预期(并检查)问题的测试。

许多模块的集成测试是一种测试,它期望模块之间的合作会产生问题,这样这些问题就很难单独使用单元测试来发现。

从以下方面考虑测试的性质:

  • 降低风险:这就是测试的目的。只有单元测试和集成测试的组合才能使您完全降低风险,因为一方面单元测试本身不能测试模块之间的适当交互,另一方面集成测试只能在很小的程度上运行非平凡模块的功能。
  • 测试编写工作:集成测试可以节省工作,因为您可能不需要编写存根/伪造/模拟。但是单元测试在实现(和维护)时也可以节省工作。这些存根(stub)/伪造(fake)/模拟(mock)恰好比在没有它们的情况下配置测试设置更容易。
  • 测试执行延迟:涉及重量级操作(如访问DBS或远程服务器等外部系统)的集成测试往往速度较慢(ER)。这意味着单元测试可以更频繁地执行,这可以减少任何失败时的调试工作,因为您对同时发生的更改有更好的了解。如果您使用测试驱动开发(TDD),这就变得尤为重要。
  • 调试工作:如果一个集成测试失败,但没有一个单元测试失败,这可能会非常不方便,因为涉及的代码太多,可能包含问题。如果您以前只更改了几行代码,这不是一个大问题——但是由于集成测试运行缓慢,您可能没有在这么短的时间间隔内运行它们……

记住,集成测试可能仍然会截取/伪造/模拟它的一些依赖项。这在单元测试和系统测试(最全面的集成测试,测试所有系统)之间提供了大量的中间基础。

使用两者的实用方法

因此,一种实用的方法是:尽可能灵活地依赖集成测试,并在风险太大或不方便的情况下使用单元测试。这种思维方式可能比对单元测试和集成测试的某种教条性区分更有用。


在我看来,答案是"为什么重要?"

是因为单元测试是你做的,而集成测试是你不做的吗?反之亦然?当然不是,你应该两者兼顾。

是因为单元测试需要快速、隔离、可重复、自我验证和及时,而集成测试不应该这样做吗?当然不是,所有的测试都应该是这些。

这是因为在单元测试中使用模拟,但在集成测试中不使用模拟?当然不是。这意味着,如果我有一个有用的集成测试,我不允许在某些部分添加一个模拟,担心我必须将我的测试重命名为"单元测试",或者将它交给另一个程序员来处理。

是因为单元测试测试一个单元,集成测试测试多个单元吗?当然不是。那有什么实际意义呢?关于测试范围的理论讨论在实践中无论如何都会失败,因为术语"单元"完全依赖于上下文。在类级别,一个单元可能是一个方法。在组装级别,单元可能是类,在服务级别,单元可能是组件。即使是类也使用其他类,那么哪个是单元呢?

这不重要。

测试是很重要的,F.I.R.S.T是很重要的,对定义的吹毛求疵是浪费时间,这只会让新来的测试者感到困惑。


集成测试:测试数据库持久性。单元测试:模拟数据库访问。测试代码方法。


如果Class1的单元测试测试正在测试Class1的特性,并且Class2的单元测试正在测试它的特性,并且它们没有命中数据库,我想我仍然会将一些交互类称为单元测试。

当一个测试运行在我的大多数堆栈中,甚至访问数据库时,我将它称为集成测试。

我真的很喜欢这个问题,因为TDD的讨论有时让我觉得有点过于纯粹了,我很高兴看到一些具体的例子。


我也这么做了-我称它们为所有单元测试,但在某个时候我有一个"单元测试",它覆盖了太多内容,我经常将它重命名为".integrationtest"-只是名称更改,其他的都没有更改。

我认为从"原子测试"(测试一个小类或一个方法)到单元测试(类级)和集成测试,再到功能测试(通常从上到下覆盖更多的东西),都有一个延续,似乎没有一个明确的界限。

如果您的测试设置了数据,并且可能加载了数据库/文件等,那么它可能更像一个集成测试(我发现集成测试使用的模拟更少,类更多,但这并不意味着您不能模拟出某些系统)。


单元测试是一种测试方法,用于验证源代码的各个单元是否正常工作。

集成测试是软件测试的一个阶段,其中单个软件模块作为一个组进行组合和测试。

维基百科将一个单元定义为应用程序中最小可测试的一部分,在爪哇/C中是一种方法。但是在你的单词和句子类的例子中,我可能只是编写句子测试,因为我可能会发现使用模拟单词类来测试句子类太过分了。所以句子就是我的单位,单词就是这个单位的实现细节。


单元测试是根据工作单元或代码块(如果您愿意)进行测试。通常由单个开发人员执行。

集成测试是指当开发人员将代码提交到源代码管理存储库时,最好在集成服务器上执行的测试。集成测试可以由巡航控制等实用程序执行。

因此,您进行单元测试以验证您构建的工作单元是否正常工作,然后集成测试验证您添加到存储库中的任何内容都不会破坏其他内容。


我称单元测试为白盒测试类的测试。类所需的任何依赖项都将替换为假依赖项(模拟)。

集成测试是指同时测试多个类及其交互的测试。在这些情况下,只有一些依赖是伪造/模拟的。

我不会调用控制器的集成测试,除非它们的一个依赖项是真实的(即,不是伪造的)(例如,iformsauthentication)。

分离这两种类型的测试对于在不同级别测试系统很有用。此外,集成测试往往是长寿命的,单元测试应该是快速的。执行速度的区别意味着它们的执行方式不同。在我们的开发过程中,单元测试在签入时运行(这很好,因为它们非常快),集成测试每天运行一次/两次。我尝试尽可能频繁地运行集成测试,但通常会命中数据库/写入文件/使RPC/etc变慢。

这就引出了另一个重要的问题,单元测试应该避免碰到IO(如磁盘、网络、数据库)。否则他们会慢下来。要设计出这些IO依赖性需要付出一些努力——我不能承认我一直忠实于"单元测试必须快速"规则,但是如果是这样的话,在更大的系统上的好处会很快显现出来。


用类比法简单解释

上面的例子做得很好,我不需要重复它们。所以我将集中在使用示例来帮助您理解。

集成测试

集成测试检查是否一切都在一起工作。想象一系列齿轮在一块表中一起工作。集成测试应该是:手表是否显示正确的时间?三天内时间是否正确?

它只告诉你整个部件是否工作。如果失败了:它不能准确地告诉你失败的地方。

单元测试

这些是真正特定的测试类型。他们告诉你某件事是有效的还是失败的。这种类型的测试的关键在于,它只测试一个特定的东西,而假设其他一切都正常工作。这是关键。

例子:让我们用一个例子来详细说明这一点:

  • 我们以汽车为例。
  • 汽车集成测试:例如,汽车是不是会前后呼啸?如果它这样做,你可以安全地说,一辆车是从一个整体的角度工作。这是一个集成测试。如果失败了,你不知道它实际上在哪里失败:它是散热器,变速器,发动机,还是化油器?你不知道。它可以是任何东西。
  • 汽车的单元测试:发动机在工作吗?这个测试假设车里的其他东西都正常工作。这样,如果这个特定的单元测试失败:您可以非常确信问题在于引擎,因此您可以快速隔离和修复问题。

使用短截线

  • 假设您的汽车集成测试失败。它不能成功地驶向Echuca。问题出在哪里?

  • 现在让我们假设您的发动机使用特殊的燃油喷射系统,并且此发动机单元测试也失败了。换句话说,集成测试和发动机单元测试都失败了。那么问题在哪里呢?(给自己10秒钟时间来得到答案。)

  • 发动机或燃油喷射系统有问题吗?

你看到这里的问题了吗?你不知道什么是失败。如果您使用不同的外部依赖项,那么这10个依赖项中的每一个都可能导致问题的发生——而且您不知道从哪里开始。这就是为什么单元测试使用存根来假设其他一切都正常工作的原因。


这个问题有点学术性,不是吗?-)我的观点是:对我来说,集成测试是对整个部分的测试,而不是十分之二的部分在一起。我们的集成测试表明,如果主构建(包含40个项目)成功的话。对于这些项目,我们有大量的单元测试。对于我来说,关于单元测试最重要的是,一个单元测试不能依赖于另一个单元测试。所以对于我来说,上面描述的两个测试都是单元测试,如果它们是独立的。对于集成测试来说,这并不重要。


此外,重要的是要记住,单元测试和集成测试都可以使用JUnit等自动化和编写。在JUnit集成测试中,可以使用org.junit.Assume类来测试环境元素(如数据库连接)或其他条件的可用性。


Have these tests essentially become integration tests because they now test the integration of these 2 classes? Or is it just a unit test that spans 2 classes?

我想是和是的。跨越2个类的单元测试变成了集成测试。

您可以通过使用模拟实现(mockword类)测试句子类来避免这种情况,当系统的那些部分足够大,可以由不同的开发人员实现时,这一点非常重要。在这种情况下,单词单独进行单元测试,句子在mockword的帮助下进行单元测试,然后句子与单词进行集成测试。

实际差异的例子如下1)1000000个元件的阵列易于单元测试,工作良好。2)起泡器很容易在10个元件的模拟阵列上进行单元测试,而且工作正常。3)集成测试表明有些事情不太好。

如果这些部分是由一个人开发的,那么在单元测试BubbleSoft时很可能会发现问题,因为开发人员已经有了真正的数组,并且不需要模拟实现。


如果您是一个TDD纯粹主义者,那么在编写生产代码之前就要编写测试。当然,测试不会编译,所以您首先让测试编译,然后让测试通过。

您可以通过单元测试来实现这一点,但不能通过集成或验收测试来实现。如果您尝试使用集成测试,在完成之前不会编译任何东西!