集成测试是否要重复所有单元测试?


36

假设我有一个函数(用Ruby编写,但每个人都应该理解):

def am_I_old_enough?(name = 'filip')
   person = Person::API.new(name)
   if person.male?
      return person.age > 21
   else
      return person.age > 18
   end
end

在单元测试中,我将创建四个测试以涵盖所有场景。每个Person::API对象都将使用带有stubbed方法male?和的模拟对象age

现在谈到编写集成测试。我认为不应再嘲笑Person :: API。因此,我将创建完全相同的四个测试用例,但不模拟Person :: API对象。那是对的吗?

如果是,那么编写单元测试的意义何在?如果我可以编写使我更有信心的集成测试(因为我在处理真实对象,而不是存根或模拟对象)?


3
好吧,要点之一是,通过对其进行模拟/单元测试,可以将任何问题隔离到代码中。如果集成测试失败,则您不知道谁的代码,您的代码或API损坏。
克里斯·沃勒特

9
只有四个测试?您应该测试六个边界年龄:17、18、19、20、21、22 ...;)
David Arno

22
@FilipBartuzi,我假设该方法例如是检查男性是否超过21岁?正如目前所写,它并没有这样做,只有22岁以上的人才是正确的。英文中的“ 21岁以上”表示“ 21+”。因此,您的代码中存在一个错误。通过测试边界值(例如,男性为2​​0、21、22,女性为17,18,19)来捕获此类错误。因此,至少需要进行六个测试。
David Arno

6
更不用说0和-1的情况了。一个人-1岁意味着什么?如果您的API返回不合理的内容,您的代码应该怎么做?
RubberDuck

9
如果将person对象作为参数传递,则测试起来会容易得多。
JeffO

Answers:


72

不,集成测试不应只是复制单元测试的内容。他们可能会重复一些报道,但这不是重点。

单元测试的重点是确保特定的一小部分功能按预期的方式完全正确地工作。单元测试am_i_old_enough将测试不同年龄的数据,当然是接近阈值的数据,可能是所有年龄的人。编写完此测试后,am_i_old_enough再也不必担心的完整性。

集成测试的重点是要验证整个系统或大量组件的组合在一起使用时是否做对。客户不在乎您编写的特定实用程序功能,他们关心的是自己的Web应用程序已得到适当保护,以防止未成年人访问,因为否则监管机构将拥有自己的资产。

检查用户的年龄只是该功能的小部分,但是集成测试不会检查您的实用程序功能是否使用了正确的阈值。它测试调用者是否基于该阈值做出正确的决定,是否完全调用了实用程序功能,是否满足其他访问条件等。

我们需要两种类型的测试的原因基本上是,执行可能通过的代码库路径可能组合的爆炸式增长。如果效用函数具有大约100个可能的输入,并且有数百个效用函数,那么要检查在所有情况下都发生正确的事情,将需要成千上万的测试用例。通过简单地检查非常小的范围内的所有情况,然后检查这些范围的常见,相关或可能的组合同时假设这些小的范围已经正确(如单元测试所示),我们可以非常自信地评估系统正在运行应该怎么做,而不会淹没在替代方案中进行测试。


6
“我们可以很自信地评估系统正在做应做的事情,而不会淹没在可供选择的场景中进行测试。” 谢谢。我喜欢有人明智地进行自动化测试。
jpmc26年

1
JB Rainsberger很好地谈论了测试以及您在最后一段中写到的组合爆炸,称为“集成测试是骗局”。与集成测试无关,但仍然很有趣。
巴特·范·尼罗普

The customer doesn't care about a particular utility function you wrote, they care that their web app is properly secured against access by minors->这是非常聪明的心态,谢谢!问题是当您为自己做项目时。这很难作为一个程序员,是在同一时刻,一个产品经理之间的分裂你的心态
菲利普Bartuzi

14

最简洁的答案是不”。更有趣的部分是为什么/如何可能出现这种情况。

我认为之所以会引起困惑,是因为您试图遵守严格的测试实践(单元测试与集成测试,模拟等),而这些代码似乎并不遵循严格的实践。

这并不是说代码是“错误的”,或者说特定的实践比其他的更好。简单地说,测试实践所做出的某些假设可能不适用于这种情况,并且可能有助于在编码实践和测试实践中使用类似级别的“严格性”;或至少承认它们可能是不平衡的,这将导致某些方面不适用或多余。

最明显的原因是您的函数正在执行两个不同的任务:

  • Person根据他们的名字查找一个。这需要进行集成测试,以确保可以找到Person可能在其他地方创建/存储的对象。
  • Person根据性别来计算a 是否足够大。这需要进行单元测试,以确保计算能够按预期进行。

通过将这些任务组合到一个代码块中,您将无法运行另一个任务。当您想对测试进行单元测试时,您不得不Person(从真实的数据库或存根/模拟中)查找一个。当您想测试查找是否与系统的其余部分集成时,您还必须对年龄进行计算。我们该怎么做呢?我们应该忽略它还是检查它?这似乎是您在问题中描述的确切困境。

如果我们设想一种替代方案,则我们可以自己进行计算:

def is_old_enough?(person)
   if person.male?
      return person.age > 21
   else 
      return person.age > 18
   end
end

由于这是一个纯计算,因此我们不需要对其进行集成测试。

我们可能也很想单独编写查找任务:

def person_from_name(name = 'filip')
   return Person::API.new(name)
end

但是,在这种情况下,功能是如此接近Person::API.new,我想您应该使用它(如果需要默认名称,是否可以将其更好地存储在其他地方,例如class属性?)。

Person::API.new(或person_from_name)编写集成测试时,您需要关注的是是否能够获得预期的结果Person; 所有基于年龄的计算都在其他地方处理,因此您的集成测试可以忽略它们。


11

我想补充说明Killian的另一点是,单元测试运行速度非常快,因此我们可以拥有1000多个。集成测试通常需要更长的时间,因为它正在调用Web服务,数据库或其他外部依赖项,因此对于集成方案,我们无法运行相同的测试(1000),因为它们会花费太多时间。

另外,单元测试通常在构建时(在构建机器上)运行,而集成测试则在环境/机器上部署后运行。

通常,每个构建都会运行我们的1000个单元测试,然后在每次部署后运行100个左右的高价值集成测试。我们可能不会将每个构建都带到部署中,但这是可以的,因为将运行集成测试所采用的构建。通常,我们希望将这些测试限制在10或15分钟内运行,因为我们不想将部署拖延太长时间。

此外,我们可能会每周执行一次集成测试的回归套件,以涵盖周末或其他停机时间的更多方案。这些时间可能会超过15分钟,因为将涵盖更多场景,但是通常没有人在星期六/星期日上工作,因此我们可以花更多时间进行测试。


不适用于动态语言(即没有构建阶段)
Filip Bartuzi
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.