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

353

我知道所谓的单元测试和集成测试的教科书定义。 但我想知道的是什么时候该写单元测试…… 我会尽可能多地编写单元测试来覆盖许多类集。

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

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

一般而言,由于这种不确定性,我很少编写实际的集成测试……或者说,使用成品来查看所有组件是否正常工作就是实际的集成测试,即使它们是手动的,并且很少超出每个单独功能的范围重复。

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

21个回答

342

对我来说,关键的区别在于集成测试可以揭示一个功能是正常工作还是出现故障,因为它们在接近实际场景的情况下测试代码。它们调用一个或多个软件方法或功能,并测试它们是否按预期工作。

相反,单元测试测试单个方法,依赖于(通常错误的)假设其余软件已经正确工作,因为它明确地模拟了每个依赖项。

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

假设您在某个地方有一个类似于以下内容的方法:

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;
}

你可以使用依赖注入、工厂方法、模拟框架或仅扩展被测试的类来实现这一点。
假设Log.DoSomething()中存在一个错误,幸运的是,Gherkin规范将发现它,并且你的端到端测试将失败。
该功能将无法正常工作,因为Log已损坏,而不是因为[使用某些输入执行你的工作]没有完成任务。顺便说一下,[使用某些输入执行你的工作]是该方法的唯一责任。
此外,假设Log在其他100个特性中使用,在其他100个类的100个其他方法中使用。
是的,100个特性将失败。但是,幸运的是,100个端到端测试也会失败并揭示问题。而且,没错:它们在说真话
这是非常有用的信息:我知道我的产品已经损坏了。这也是非常令人困惑的信息:它告诉我问题所在。它向我传达了症状,而不是根本原因。
然而,DoSomething的单元测试是绿色的,因为它使用了一个假的Log,被构建成永远不会出错。是的:它显然是在欺骗。它传递了一个损坏的功能正在运行。它怎么可能有用呢?
(如果DoSomething()的单元测试失败,请确保:[以某些输入执行你的工作]有一些错误。)
假设这是一个存在损坏类的系统: A system with a broken class 一个单一的缺陷将破坏多个功能,并导致多个集成测试失败。

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

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

The same bug will break just one unit test

现在,比较这两种情况。

同样的错误只会导致一个单元测试失败。

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

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

区别

集成测试告诉你哪些不起作用。但是它们在猜测问题可能出在哪里方面没有用处。

单元测试是唯一能够告诉你bug出现在哪里的测试。要获取这些信息,它们必须在模拟环境中运行该方法,在该环境中,所有其他依赖项应该正确工作。

这就是为什么我认为你的句子“或者它只是跨越2个类的单元测试”有点错位。一个单元测试永远不应该跨越两个类。

这个回复基本上是我在这里写的内容的总结:单元测试会撒谎,所以我喜欢它们

9
一个非常好的答案! 但是我想补充一点,模拟不仅仅适用于单元测试,它在许多集成测试用例中也非常有用。 - n.Stenvang
1
很棒的回答!我只是不完全同意两点:1)集成测试“对猜测问题可能出在哪里没有任何用处”;2)一个单元测试不应涵盖两个类。我创建了很多集成测试,当它们失败时通常不难定位问题的来源,只要你获得完整的堆栈跟踪或者单个失败的断言(在这种情况下,找到源可能会更困难,但也不是太难,因为集成测试提供了一个包含的调试上下文)。 (继续) - Rogério
8
只要它们不是应该拥有自己独立单元测试的公共类,单元测试可以涵盖多个类。一种情况是当公共测试类使用其他非公共辅助类来支持公共类时; 在这种情况下,“单元”包括两个或更多类。另一种情况是大多数类使用第三方类(String/string类、集合类等),这些类不需要被模拟或与之隔离; 我们只将它们视为稳定可靠的依赖项,超出了测试范围。 - Rogério
2
通过集成测试,要找到根本问题会有一些困难,但你仍然可以进行调试并找到根本问题。假设单元测试不经常失败,那么如果你只有集成测试,修复错误可能需要更长的时间,但是你还可以获得测试组件集成的附加价值,并节省编写单元测试的时间。我认为这个说法(来自我的老板)是错误的,但我不确定如何说服他,有什么想法吗? - BornToCode
2
根据这个答案的推理,有人可能会认为跳过编写单元测试并花费节省下来的时间来定位失败的集成测试源可能更加有效。 - user74754
显示剩余2条评论

65

当我编写单元测试时,通过模拟依赖项来限制被测试代码的范围为我当前正在编写的类。如果我正在编写一个 Sentence 类,而 Sentence 类依赖于 Word,则我将使用模拟的 Word。通过模拟 Word,我可以仅关注其接口并测试 Sentence 类与 Word 接口交互时的各种行为。这样,我只测试了 Sentence 的行为和实现,而没有同时测试 Word 的实现。

一旦我编写了单元测试以确保 Sentence 在基于 Word 的接口进行交互时的正确行为,那么我就编写集成测试,以确保我的交互假设是正确的。对此,我提供实际的对象,并编写一个测试,该测试会使用 Sentence 和 Word 两个类,并执行一个将使用这两个类的功能。


51

在单元测试中,您会对每个部分进行隔离测试:enter image description here

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

enter image description here

当你只使用单元测试时会发生什么(通常情况下,两个窗口都可以工作,但不幸的是不能同时工作):

enter image description here

来源: source1 source2


你有三张图片,但只有两个来源。 - gerrit
2
@gerrit请查看第一个源代码——两张图片来自那里。 - Michu93
4
我很喜欢这个回答。 - Dzenis H.
整辆汽车撞墙的图片对我来说更像是系统测试。 - olyv

47

我的10个看法 :D

我一直被告知,单元测试是对单个组件的测试 - 应该全面测试其功能。现在,这往往有许多层次,因为大多数组件由更小的部分组成。对我而言,一个单元是系统中的一个功能部分。因此,它必须提供某些价值(例如不是用于字符串解析的方法,而是可能是HtmlSanitizer)。

集成测试是下一步,它将一个或多个组件组合在一起,并确保它们像应该的那样协同工作。当您将HTML输入到HtmlEditControl中时,您不必担心组件如何单独工作,但它会以某种神奇的方式自动检查它是否有效。

这是一条真正可移动的线.. 我更喜欢更专注于让该死的代码完美运行 ^_^


23

单元测试使用模拟

你说的是整合测试,它实际上测试了系统的整个集成。但是在进行单元测试时,你应该分别测试每个单元。其他所有内容都应该被模拟。所以,在你的Sentence类中,如果它使用Word类,那么你的Word类应该被模拟。这样,你只会测试你的Sentence类功能。


2
我知道这是一个旧帖子,但我刚刚看到它。如果你有一个名为Font的第三个类,Sentence类与之交互,并且你想要测试Word和Sentence类之间的功能,则必须模拟Font类,但这并不会使其成为单元测试。 因此,我的意思是使用模拟并不一定使其成为单元测试,模拟也可以用于集成测试。 - n.Stenvang
3
当然可以在集成测试中使用模拟对象,但为了使单元测试真正成为单元测试,除单元本身外的所有内容都应该被模拟。如果集成测试使用模拟对象,则它们很可能是部分集成测试(如果这个术语存在的话)。当然,有一些部分集成测试是自上而下或自下而上的。后者通常不需要使用模拟对象,而前者则需要。 - Robert Koritnik
不是所有的单元测试都需要模拟。事实上,通常最好设计您的系统,使得可以在没有模拟的情况下进行代码测试。 - Adam Zerner

18

我认为,当你开始思考集成测试时,你更多地在谈论物理层面而不是逻辑层面。

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

从技术上讲,你无法单元测试一个类。如果你的类由几个其他类组成,那又怎么办?这样是否自动成为了一个集成测试?我不这么认为。


8
从技术上讲,你无法单元测试只有一个类。如果你的类由几个其他类组成怎么办?一个“严格”的单元测试将只模仿/存根所有依赖项。然而,这是否总是实用有争议。 - sleske
3
没错,sieske。重要的是尽可能将依赖项保持在最低限度。 - Jon Limjap
-1,单元测试不是测试单个功能,而是测试单个软件函数或类,即测试软件的逻辑单元。 - CharlesB

15
使用单一职责原则,只有一项职责的话,就是明确的。多于一项职责,就是集成测试。
通过鸭子测试(外观、声音、步态都像鸭子,则可以判定为鸭子),它仅仅是一个包含了多个新对象的单元测试。
当你进入MVC并测试它时,控制器测试总是集成测试,因为控制器包含模型单元和视图单元。在模型中测试逻辑,我会称之为单元测试。

11

你测试的本质

模块X的单元测试是一项测试,该测试仅期望(并检查)模块X中的问题。

许多模块的集成测试是一项测试,该测试期望由这些模块之间的合作引起的问题,因此仅使用单元测试很难发现这些问题。

以以下方式考虑你测试的本质:

  • 降低风险:这就是测试的目的。只有单元测试和集成测试的结合才能为您提供完全的风险降低,因为一方面,单元测试本质上无法测试模块之间的适当交互,另一方面,集成测试只能对一个非平凡模块的功能进行少量测试。
  • 测试编写工作量:集成测试可以节省工作量,因为您可能不需要编写存根/虚拟/模拟。但是,当实现(和维护!)这些存根/虚拟/模拟比没有它们配置测试设置更容易时,单元测试也可以节省工作量。
  • 测试执行延迟:涉及大型操作(例如访问外部系统,如DB或远程服务器)的集成测试往往会变慢。这意味着单元测试可以更频繁地执行,如果出现任何问题,则减少调试工作量,因为您对此期间进行了更好的更改了解。如果您使用测试驱动开发(TDD),这一点尤其重要。
  • 调试工作量:如果集成测试失败,但没有单元测试失败,那么这可能非常不方便,因为涉及到的代码可能包含问题。如果您之前只更改了几行代码,则这不是一个大问题-但是由于集成测试运行缓慢,您可能没有在如此短的时间间隔内运行它们......

请记住,集成测试仍然可以存根/虚拟/模拟掉部分依赖项。这为单元测试和系统测试(最全面的集成测试,测试整个系统)提供了丰富的中间地带。

实用的方法来同时使用两者

因此,一种实用的方法是:在可以合理地使用集成测试的情况下灵活依赖它们,并在单元测试可能过于冒险或不方便时使用单元测试。这种思考方式可能比对单元测试和集成测试进行死板的区分更有用。


11

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

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

是因为单元测试需要快速、独立、可重复、自验证和及时,而集成测试不需要吗?当然不是,所有测试都应该具备这些特点。

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

是因为单元测试测试一个单元,集成测试测试多个单元?当然不是。这在实践中有什么实际重要性呢?关于测试范围的理论讨论在实践中已经崩溃了,因为术语“单元”完全取决于上下文。在类级别,一个单元可能是一个方法。在组件级别,一个单元可能是一个类,而在服务级别,一个单元可能是一个组件。甚至类也使用其他类,那么哪个是单元?

这并不重要。

测试很重要,F.I.R.S.T很重要,在定义上斤斤计较只是浪费时间,只会让新手对测试感到困惑。


5
定义使人们能够使用相同的术语而无需总是解释其含义,这对于合作至关重要。因此,了解两个概念之间的区别至关重要。 - CharlesB
正如@CharlesB所提到的那样,这很重要,因此无需每次都解释或发现每个人都有不同的定义而导致混淆。测试将以不同的方式编写和运行,但这并不意味着希望定义差异时不应该同时进行两者。 - Shane
1
答案的结论可能有些极端,但它的大部分观点都是相当合理的:单元测试和集成测试在本质上是相同的,唯一的区别在于它们的粒度——而且并不明显应该在哪里划分它们之间的界限。 - Lutz Prechelt
这并不能帮助在专业环境中创建一个共同的语言。虽然大多数情况下你是对的,但没有一个共同的语言会在团队中造成误解和混乱。我认为最好的选择是让团队就术语和定义达成一致。 - KRK Owner

5

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

集成测试是软件测试的阶段,在该阶段中,将单个软件模块组合并作为一个整体进行测试。

维基百科定义一个单元是应用程序中可测试的最小部分,在Java/C#中,这是一个方法。但在Word和Sentence类的示例中,我可能只会为句子编写测试,因为我可能会发现使用mock对象来测试句子类的单词类过于繁琐。所以,句子将成为我的单元,而单词则是该单元的实现细节。


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