335

我知道单元测试和集成测试的所谓教科书定义。我很好奇的是什么时候应该编写单元测试......我将编写它们以涵盖尽可能多的类集。

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

这些测试是否本质上成为集成测试,因为它们现在测试这 2 个类的集成,还是只是一个跨越 2 个类的单元测试?

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

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

4

20 回答 20

328

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

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

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

假设您有这样的方法:

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

DoSomething对您的客户来说非常重要:这是一项功能,是唯一重要的事情。这就是为什么您通常编写 Cucumber 规范来断言它:您希望验证传达该功能是否有效。

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你应该假装(使用一些模拟)其余的类和方法正在工作(即:该方法使用的所有依赖项都正常工作)并断言你的方法正在工作。

在实践中,您可以执行以下操作:

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

您可以使用依赖注入、某些工厂方法或任何 Mock 框架或仅扩展被测类来做到这一点。

假设Log.DoSomething(). 幸运的是,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]有一些错误。)

假设这是一个类损坏的系统: 一个有损坏类的系统

一个错误会破坏几个功能,并且几个集成测试会失败。

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

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

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

现在,比较这两种情况。

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

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

实际上,使用损坏功能的所有模块的单元测试都是绿色的,因为通过使用模拟,它们删除了依赖项。换句话说,它们运行在一个理想的、完全虚构的世界中。这是隔离错误并寻找它们的唯一方法。单元测试意味着模拟。如果你不嘲笑,你就不是单元测试。

区别

集成测试告诉我们什么是行不通的。但是它们对于猜测问题可能出在哪里是没有用的。

单元测试是唯一能告诉你错误到底在哪里的测试。要获取此信息,他们必须在模拟环境中运行该方法,其中所有其他依赖项都应该正确工作。

这就是为什么我认为你的句子“或者它只是一个跨越 2 个类的单元测试”在某种程度上被取代了。单元测试不应该跨越 2 个类。

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

于 2011-10-24T13:05:37.817 回答
64

当我编写单元测试时,我通过模拟依赖项将正在测试的代码范围限制在我当前正在编写的类中。如果我正在编写一个 Sentence 类,并且 Sentence 依赖于 Word,我将使用模拟 Word。通过模拟 Word,我可以只关注它的界面并测试我的 Sentence 类在与 Word 的界面交互时的各种行为。这样我只测试 Sentence 的行为和实现,而不是同时测试 Word 的实现。

一旦我编写了单元测试以确保 Sentence 在基于 Word 的界面与 Word 交互时行为正确,然后我编写集成测试以确保我对交互的假设是正确的。为此,我提供了实际对象并编写了一个测试,该测试将使用最终将使用 Sentence 和 Word 的功能。

于 2008-09-01T19:33:42.823 回答
47

我的 10 位 :D

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

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

虽然它是一条真正的可移动线..我宁愿更多地专注于让该死的代码完全停止工作^_^

于 2008-08-14T08:59:28.557 回答
35

在单元测试中,您测试隔离的每个部分: 在此处输入图像描述

在集成测试中,您测试系统的许多模块:

在此处输入图像描述

当您只使用单元测试时会发生这种情况(通常两个窗口都在工作,不幸的是不能一起工作):

在此处输入图像描述

来源:来源 1来源 2

于 2019-07-31T05:28:36.733 回答
23

单元测试使用模拟

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

于 2009-08-03T17:07:18.523 回答
18

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

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

从技术上讲,无论如何您都不能只对一类进行单元测试。如果您的班级由其他几个班级组成怎么办?这会自动使其成为集成测试吗?我不这么认为。

于 2008-08-14T06:34:05.853 回答
14

采用单一职责设计,黑白分明。不止一个责任,它是一个集成测试。

通过鸭子测试(看起来,嘎嘎,蹒跚,它是一只鸭子),它只是一个单元测试,其中包含超过 1 个新对象。

当您进入 mvc 并对其进行测试时,控制器测试始终是集成的,因为控制器包含模型单元和视图单元。在该模型中测试逻辑,我称之为单元测试。

于 2008-08-14T06:32:30.317 回答
10

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

是因为单元测试是你做的,而集成测试是你不做的吗?或相反亦然?当然不是,你应该尝试两者都做。

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

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

是因为单元测试测试一个单元而集成测试测试多个单元吗?当然不是。这有什么实际意义?无论如何,关于测试范围的理论讨论在实践中都失败了,因为“单元”一词完全取决于上下文。在类级别,一个单元可能是一个方法。在装配级别,一个单元可能是一个类,而在服务级别,一个单元可能是一个组件。甚至类都使用其他类,那么哪个是单位?

这无关紧要。

测试很重要,FIRST 很重要,对定义争论不休是浪费时间,只会让新手对测试感到困惑。

于 2013-07-21T00:00:53.987 回答
10

测试的性质

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

许多模块的集成测试是一种预期模块之间的协作会出现问题的测试,因此单独使用单元测试很难发现这些问题。

用以下术语考虑测试的性质:

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

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

实用的方法来使用两者

所以一个务实的方法是:尽可能灵活地依赖集成测试,并在风险太大或不方便的地方使用单元测试。这种思维方式可能比单元测试和集成测试的一些教条区分更有用。

于 2014-09-22T14:49:20.580 回答
4

我认为我仍然会将几个交互类称为单元测试,前提是 class1 的单元测试正在测试 class1 的功能,而 class2 的单元测试正在测试其功能,并且它们没有访问数据库。

当测试通过我的大部分堆栈甚至命中数据库时,我将测试称为集成测试。

我真的很喜欢这个问题,因为 TDD 讨论有时对我来说有点过于纯粹,看到一些具体的例子对我有好处。

于 2008-08-14T06:37:17.407 回答
4

我也这样做——我把它们都称为单元测试,但在某些时候我有一个“单元测试”,它涵盖了这么多,我经常将它重命名为“..IntegrationTest”——只是一个名称的改变,没有其他改变。

我认为从“原子测试”(测试一个小类或一种方法)到单元测试(类级别)和集成测试 - 然后是功能测试(通常从上到下涵盖更多内容) - 似乎没有一个干净的切断。

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

于 2008-08-14T06:47:54.093 回答
4

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

集成测试是软件测试的阶段,其中将各个软件模块组合在一起并作为一个组进行测试。

Wikipedia将单元定义为应用程序的最小可测试部分,在 Java/C# 中它是一种方法。但是在您的 Word 和 Sentence 类示例中,我可能只会为句子编写测试,因为我可能会发现使用模拟词类来测试句子类有点矫枉过正。所以句子将是我的单位,而单词是该单位的实现细节。

于 2008-08-14T10:26:18.613 回答
4

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

于 2012-07-13T18:56:23.410 回答
3

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

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

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

于 2008-08-14T06:36:58.140 回答
2

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

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

我不会调用 Controller 的集成测试,除非它们的依赖项之一是真实的(即不是伪造的)(例如 IFormsAuthentication)。

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

这提出了另一个重要的观点,单元测试应该避免碰到 IO(例如磁盘、网络、数据库)。否则他们会慢很多。设计这些 IO 依赖项需要一些努力——我不能承认我一直忠实于“单元测试必须快速”的规则,但如果你这样做了,那么在更大的系统上的好处会很快变得明显.

于 2008-08-14T12:19:55.603 回答
2

类比的简单解释

上面的例子做得很好,我不需要重复它们。因此,我将专注于使用示例来帮助您理解。

集成测试

集成测试检查一切是否协同工作。想象一下手表中的一系列齿轮一起工作。集成测试将是:手表显示的时间是否正确?3天后它仍然告诉正确的时间吗?

它告诉你的只是整个作品是否有效。如果失败:它不会准确地告诉你失败的地方。

单元测试

这些确实是特定类型的测试。它们会告诉您一件特定的事情是有效还是失败。这种测试的关键是它只测试一个特定的东西,同时假设其他一切都很好。这是关键点。

示例: 让我们通过一个示例来详细说明这一点:

  • 让我们以汽车为例。
  • 汽车的集成测试:例如,汽车是否开往 Woop Woop 并返回?如果它这样做,您可以肯定地说汽车从整体角度来看正在工作。这是一个集成测试。如果它失败了,你不知道它实际上在哪里失败:是散热器、变速箱、发动机还是化油器?你不知道。它可以是任何东西。
  • 汽车的单元测试:引擎是否在工作?该测试假定汽车中的其他一切工作正常。这样,如果这个特定的单元测试失败:您可以非常确信问题出在引擎上——因此您可以快速隔离并修复问题。

使用存根

  • 假设您的汽车集成测试失败。它没有成功开往伊丘卡。问题出在哪里?

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

  • 是发动机的问题,还是燃油喷射系统的问题?

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

于 2016-12-06T00:58:19.397 回答
1

这些测试是否本质上成为集成测试,因为它们现在测试这两个类的集成?或者它只是一个跨越 2 个类的单元测试?

我认为是的,是的。跨越 2 个类的单元测试变成了集成测试。

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

实际差异的示例如下 1) 1,000,000 个元素的数组很容易进行单元测试并且工作正常。2) BubbleSort 很容易在 10 个元素的模拟数组上进行单元测试,并且工作正常 3) 集成测试表明有些东西不太好。

如果这些部分是单人开发的,在单元测试 BubbleSoft 的时候很可能会发现问题,因为开发者已经有了真正的数组,不需要 mock 实现。

于 2008-09-25T09:37:29.267 回答
1

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

于 2008-08-14T06:37:59.870 回答
1

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

于 2013-09-10T17:12:33.200 回答
0

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

您可以通过单元测试来做到这一点,但不能通过集成或验收测试来做到这一点。如果您尝试使用集成测试,那么在您完成之前,什么都不会编译!

于 2018-11-02T17:08:54.510 回答