使用验收和集成测试代替单元测试是否足够?


62

这个问题的简短介绍。我现在已经使用TDD,最近使用BDD已经超过一年了。我使用诸如嘲笑之类的技术来更有效地编写测试。最近,我开始了一个个人项目,为自己编写一个小小的资金管理程序。由于我没有遗留代码,因此从TDD开始是一个完美的项目。不幸的是,我没有体验到TDD的喜悦。它甚至破坏了我的乐趣,以至于我放弃了这个项目。

怎么了 好吧,我使用了类似TDD的方法来使测试/需求发展程序的设计。问题在于,超过一半的开发时间用于编写/重构测试。所以最后我不想实现任何其他功能,因为我需要重构并编写许多测试。

在工作中,我有很多遗留代码。在这里,我写了越来越多的集成和验收测试,以及更少的单元测试。这似乎不是一个坏方法,因为大多数错误是通过验收和集成测试检测到的。

我的想法是,最终我可以编写比单元测试更多的集成和验收测试。就像我说的那样,对于检测错误,单元测试并不比集成/验收测试好。单元测试也对设计有益。由于我曾经写过很多文章,所以我的类总是被设计为可测试的。此外,在大多数情况下,让测试/需求指导设计的方法可导致更好的设计。单元测试的最后一个优点是它们更快。我已经写了足够多的集成测试,知道它们可以和单元测试一样快。

在浏览网络后,我发现这里那里提到的想法非常相似。您如何看待这个想法?

编辑

在一个例子中回答这个设计很好的例子,但是我需要对下一个需求进行大量重构:

首先,执行某些命令有一些要求。我编写了一个可扩展的命令解析器-从某种命令提示符下解析命令,并在模型上调用正确的命令。结果以视图模型类表示: 第一设计

这里没有错。所有类都彼此独立,我可以轻松添加新命令,显示新数据。

下一个要求是,每个命令应具有其自己的视图表示形式-命令结果的某种预览。我重新设计了程序,以针对新要求实现更好的设计: 第二设计

这也很好,因为现在每个命令都有自己的视图模型,因此也有自己的预览。

事实是,命令解析器已更改为使用基于令牌的命令解析,并且剥夺了其执行命令的能力。每个命令都有自己的视图模型,数据视图模型只知道当前的命令视图模型,而不知道必须显示的数据。

我现在想知道的是,新设计是否没有违反任何现有要求。我不必更改任何我的验收测试。我不得不重构或删除几乎每个单元测试,这是一大堆工作。

我想在这里展示的是在开发过程中经常发生的一种常见情况。旧的或新的设计都没有问题,它们只是随需求而自然变化-我的理解是,TDD的优点之一就是设计不断发展。

结论

感谢您的所有答案和讨论。在此讨论的总结中,我想到了一种方法,将在下一个项目中对其进行测试。

  • 首先,我在像往常一样执行任何操作之前编写所有测试。
  • 对于需求,我首先编写一些验收测试,以测试整个程序。然后,我为需要实现需求的组件编写了一些集成测试。如果有一个组件与另一个组件紧密协作以实现此要求,那么我还将编写一些集成测试,其中两个组件都需要一起测试。最后但并非最不重要的一点是,如果我必须编写一个具有高排列的算法或任何其他类(例如,序列化程序),我将为此特定类编写单元测试。所有其他类均未经测试,但未经任何单元测试。
  • 对于错误,可以简化过程。通常,错误是由一个或两个组件引起的。在这种情况下,我将为测试该错误的组件编写一个集成测试。如果它与算法有关,我只会编写一个单元测试。如果不容易检测到发生错误的组件,我将编写验收测试以查找错误-这应该是一个例外。


这些问题似乎更多地解决了为什么要编写测试的问题。我想讨论编写功能测试而不是单元测试是否是更好的方法。
Yggdrasil 2014年

根据我的阅读,重复问题的答案主要是关于何时进行非单元测试更有意义
gnat 2014年

第一个链接本身就是重复的。想你的意思:programmers.stackexchange.com/questions/66480/...
罗比·迪伊

Robbie Dee链接提供的答案甚至更多地说明了为什么要进行测试。
Yggdrasil 2014年

Answers:


37

它正在比较桔子和苹果。

集成测试,验收测试,单元测试,行为测试-它们都是测试,它们都将帮助您改进代码,但它们也大不相同。

我认为我将遍历每个不同的测试,并希望解释为什么需要混合使用所有这些测试:

集成测试:

简单地,例如,测试系统的不同组成部分是否正确集成-也许您模拟了Web服务请求并检查结果是否返回。我通常会使用真实的(ish)静态数据和模拟的依赖项来确保可以一致地对其进行验证。

验收测试:

验收测试应与业务用例直接相关。它可以是巨大的(“正确提交交易”)或很小的(“过滤器成功过滤列表”)–无关紧要;重要的是,应将其明确绑定到特定的用户需求。我喜欢集中精力进行测试驱动的开发,因为这意味着我们拥有一本很好的测试手册参考,以供开发人员和质量检查人员进行验证。

单元测试:

对于可能由单独的用户故事构成或不构成单个用户故事的小型离散功能单元,例如,某个用户故事说我们在访问特定网页时会检索所有客户,这可能是一项接受测试(模拟访问网络)页并检查响应),但也可能包含多个单元测试(验证是否检查了安全权限,验证数据库连接查询正确,验证是否限制了结果数量的任何代码均已正确执行)-这些都是“单元测试”这不是一个完整的验收测试。

行为测试:

在特定输入的情况下,定义应用程序的流程。例如,“当无法建立连接时,请验证系统重试连接。” 同样,这不太可能是一个完整的验收测试,但它仍然允许您验证有用的东西。

在我看来,这些都是通过编写测试的丰富经验得出的。我不喜欢专注于教科书的方法-而是专注于赋予测试价值的要素。


在您的定义中,我假设我的意思是编写比单元测试更多的行为测试(?)。对我来说,单元测试是一种测试所有依赖关系的类的测试。在某些情况下,单元测试最有用。例如,当我编写复杂的算法时。然后,我有很多例子,预期的算法输出。我想在单元级别上对此进行测试,因为它实际上比行为测试更快。我看不到在单元级别测试一个类的价值,该类只有一只手可以通过行为测试轻松地测试该类的所有路径。
Yggdrasil 2014年

14
我个人认为验收测试是最重要的,行为测试在测试诸如通讯,可靠性和错误情况之类的东西时是重要的,而单元测试在测试小的复杂功能时是重要的(算法将是一个很好的例子)
Michael

我对你的用语不太确定。我们编写一个编程套件。在这里,我负责图形编辑器。我的测试使用套件其余部分中的模拟服务以及模拟UI对编辑器进行测试。那将是什么样的测试?
Yggdrasil 2014年

1
取决于您要测试的内容-您是否正在测试业务功能(验收测试)?您是否正在测试集成(集成测试)?您是否正在测试单击按钮时发生的情况(行为测试)?您是否正在测试算法(单元测试)?
迈克尔

4
“我不喜欢专注于教科书的方法-而是专注于赋予测试价值的要素”哦,是这样!总是要问的第一个问题是“我通过这样做可以解决什么问题?”。而不同的项目可能有不同的问题要解决!
Laurent Bourgault-Roy 2014年

40

TL; DR:是的,只要满足您的需求。

多年来,我一直在进行验收测试驱动开发(ATDD)开发。这可能是非常成功的。有几件事要注意。

  • 单元测试确实可以帮助执行IOC。没有单元测试,开发人员有责任确保它们满足编写良好代码的要求(就单元测试而言,要确保编写好的代码)
  • 如果您实际上使用的是通常被嘲笑的资源,它们的运行速度可能会变慢并出现错误的故障。
  • 测试没有像单元测试那样指出具体问题。您需要进行更多调查以修复测试失败。

现在的好处

  • 更好的测试覆盖率,涵盖了集成点。
  • 确保系统整体符合验收标准,这是软件开发的重点。
  • 使大型重构更容易,更快和更便宜。

与往常一样,您需要进行分析并弄清楚这种做法是否适合您的情况。与许多人不同,我认为没有理想的正确答案。这将取决于您的需求和要求。


8
优点。变得有点不精通测试,并且撰写数百个案例以在通过“飞扬的色彩”时获得热烈的满足,这太容易了。简而言之:如果您的软件没有从用户的角度执行其需要做的工作,则说明您已通过了第一个也是最重要的测试。
罗比·迪

指出具体问题的好方法。如果我有很高的要求,我会编写验收测试来测试整个系统,而不是编写测试系统中某些组件的子任务以达到要求的测试。有了这个,我可以在大多数情况下,缺陷在于查明该组件。
Yggdrasil的

2
“单元测试有助于实施IOC”?!?我确定您是在此处使用DI而不是IoC,但是无论如何,为什么有人要强制使用DI?我个人发现,在实践中DI会导致非对象(过程样式编程)。
罗杰里奥

您是否可以同时进行集成测试和单元测试(仅进行集成测试)来提供(IMO最佳)选择?您在这里的回答是好的,但似乎将这些事情视为互斥的,我不认为这是相互排斥的。
starmandeluxe

@starmandeluxe它们确实不是相互排斥的。而是您想从测试中获得价值的问题。我将在价值超过编写单元测试的开发/支持成本的任何地方进行单元测试。例如 我当然会在金融应用程序中对复利功能进行单元测试。
Dietbuddha

18

好吧,我使用了类似TDD的方法来使测试/需求发展程序的设计。问题在于,超过一半的开发时间用于编写/重构测试

当用于组件的公共接口的更改不太频繁时,单元测试最有效。这意味着,如果已经对组件进行了良好的设计(例如,遵循SOLID原则)。

因此,认为一个好的设计只是从“抛出”一个组件的大量单元测试中“演变”而来,是一个谬论。TDD并不是良好设计的“老师”,它只能帮助您一点一点验证设计的某些方面(特别是可测试性)是否良好。

当您的需求发生变化时,您必须更改组件的内部结构,这将破坏90%的单元测试,因此您必须非常频繁地对其进行重构,因此设计很可能不是那么好。

因此,我的建议是:考虑您创建的组件的设计,以及如何按照开放/封闭原则使它们更多。后者的想法是确保组件的功能可以在以后扩展而无需更改它们(从而不会破坏单元测试使用的组件的API)。这些组件可以(并且应该)包含在单元测试中,并且体验不应像您描述的那样痛苦。

当您不能立即提出这样的设计时,验收和集成测试可能确实是一个更好的开始。

编辑:有时您的组件设计可以很好,但是单元测试设计可能会引起问题。一个简单的例子:您要测试类X的方法“ MyMethod”并编写

    var x= new X();
    Assert.AreEqual("expected value 1" x.MyMethod("value 1"));
    Assert.AreEqual("expected value 2" x.MyMethod("value 2"));
    // ...
    Assert.AreEqual("expected value 500" x.MyMethod("value 500"));

(假设这些值具有某种意义)。

进一步假设,在生产代码中,只有一个调用X.MyMethod。现在,对于新要求,方法“ MyMethod”需要一个附加参数(例如context),不能省略。没有单元测试,就只能在一个地方重构调用代码。使用单元测试,必须重构500个位置。

但是这里的原因不是单元测试本身,而是事实是,一次又一次地重复对“ X.MyMethod”的相同调用,而不是严格遵循“不要重复自己(DRY)”原则。这里是将测试数据和相关的期望值放在列表中,并在循环中运行对“ MyMethod”的调用(或者,如果测试工具支持所谓的“数据驱动器测试”,则可以使用该功能)。方法签名更改为1(而不是500)时,单元测试中要更改的位数。

在您的现实世界中,情况可能会更复杂,但是我希望您能想到-在单元测试中使用不知道它可能会更改的组件API时,请确保减少数量该API的调用次数最少。


“这意味着,当组件已经被良好设计时。”:我同意您的观点,但是如果您在编写代码之前编写测试并且代码就是设计,那么如何对组件进行设计?至少这就是我对TDD的理解。
Giorgio

2
@Giorgio:实际上,无论您是先编写测试还是稍后编写测试都没有关系。设计是指对组件的职责,公共接口,依赖项(直接或注入),运行时或编译时的行为,可变性,名称,数据流,控制流,层等做出决策。设计还意味着将某些决策推迟到最新的时间点。单元测试可以间接地向您显示设计是否正确:如果在需求改变后又要重构很多设计,那可能就不行了。
布朗

@Giorgio:一个例子可能会澄清:假设您有一个组件X,带有方法“ MyMethod”和2个参数。使用TDD,您可以X x= new X(); AssertTrue(x.MyMethod(12,"abc"))在实际实现该方法之前进行编写。使用前期设计,您可以class X{ public bool MyMethod(int p, string q){/*...*/}}先编写,然后再编写测试。在这两种情况下,您都做出了相同的设计决策。如果决定是好是坏,TDD不会告诉您。
布朗

1
我同意你的看法:我对TDD会盲目地应用,并认为它会自动产生良好的设计感到有些怀疑。此外,如果设计尚不明确,有时TDD会妨碍您的工作:在概述自己的工作之前,我不得不测试细节。因此,如果我理解正确,我们同意。我认为(1)单元测试有助于验证设计,但设计是一项单独的活动,并且(2)TDD并非始终是最佳解决方案,因为您需要在开始编写测试之前先组织好想法,而TDD会使您的工作慢下来这个。
Giorgio

1
不久,单元测试会显示组件内部设计中的缺陷。必须事先知道界面,前后条件,否则您将无法创建单元测试。因此,在编写单元测试之前,必须先进行组件的设计。编写单元测试后,可以进行低级设计,详细设计或内部设计或任何您想称呼的操作。
Maarten Bodewes,2014年

9

是的,当然是。

考虑一下:

  • 单元测试是针对性的小型测试,它执行一小段代码。您编写了很多代码,以实现不错的代码覆盖率,以便测试所有(或大多数尴尬的位)。
  • 集成测试是一项大型,广泛的测试,可以对您的代码进行大范围的测试。您只需编写少量代码即可实现不错的代码覆盖率,以便测试所有(或大多数尴尬的位)。

看到整体差异。

问题是代码覆盖率之一,如果您可以使用集成/验收测试对所有代码进行全面测试,那么就没有问题了。您的代码已经过测试。这就是目标。

我认为您可能需要将它们混合在一起,因为每个基于TDD的项目都需要进行集成测试,以确保所有单元实际上都能正常工作(我从经验中知道,通过100%单元测试的代码库不一定能正常工作当您将它们放在一起时!)

问题的实质在于简化测试,调试故障和修复故障。有些人发现他们的单元测试非常擅长于此,它们既小又简单,失败很容易看到,但是缺点是您必须重新组织代码以适合单元测试工具,并编写很多代码。集成测试很难编写以覆盖很多代码,并且您可能必须使用日志记录之类的技术来调试任何故障(尽管我想无论如何都必须这样做,否则无法对故障进行单元测试)现场!)。

无论哪种方式,您仍然可以获得经过测试的代码,您只需要确定哪种机制更适合您。(我会混合使用,对复杂算法进行单元测试,对其余算法进行集成测试)。


7
并非完全正确...通过集成测试,可能有两个组件都是错误的,但是它们的错误在集成测试中被抵消了。直到最终用户以仅使用这些组件中的一种的方式使用它,这并不重要...
Michael Shaw 2014年

1
代码覆盖率==已测试-除了互相抵消的错误之外,您从未想到的方案又如何呢?集成测试适合进行愉快的路径测试,但是当事情进展不顺利时,我很少看到足够的集成测试。
2014年

4
@Ptolemy我认为2个越野车组件相互抵消的稀有性远低于2个相互干扰的工作组件。
gbjbaanb 2014年

2
@Michael则您只是没有在测试中投入足够的精力,我确实说过要进行良好的集成测试更加困难,因为测试必须更加详细。您可以像在单元测试中一样轻松地在集成测试中提供错误的数据。集成测试!=幸福的道路。它涉及尽可能多地执行代码,这就是为什么有一些代码覆盖率工具向您显示已执行了多少代码的原因。
gbjbaanb 2014年

1
@Michael当我正确使用Cucumber或SpecFlow之类的工具时,我可以创建集成测试,该测试也可以像单元测试一样快速地测试异常和极端情况。但是我同意,如果一个类有很大的变化,我更愿意为此类编写单元测试。但这要比只包含少数路径的类要少。
Yggdrasil 2014年

2

我认为这是一个可怕的想法。

由于验收测试和集成测试涉及代码的更广泛部分以测试特定目标,因此随着时间的推移,它们将需要更多的重构,而不是更少。更糟糕的是,由于它们确实涵盖了代码的广泛部分,因此它们增加了您查找根本原因所需的时间,因为您可以从更大的范围进行搜索。

不,您通常应该编写更多的单元测试,除非您有一个90%UI的奇特应用程序或其他难以进行单元测试的应用程序。您遇到的痛苦不是来自单元测试,而是来自于测试优先开发。通常,在大多数写作测试中,您只应花费1/3的时间。毕竟,它们是为您服务的,反之亦然。


2
我听到的反对TDD的主要牛肉是,它会破坏自然发展流程​​,并从一开始就实施防御性编程。如果程序员已经承受了时间压力,他们可能只想剪切代码并在以后对其进行抛光。当然,使用错误的代码满足任意期限是不正确的。
罗比·迪

2
确实,特别是因为“以后再抛光”似乎从未真正发生过-我所做的每一次评论都在开发人员推动“它需要淘汰,我们稍后再做”的情况下,当我们都知道那将不会发生-技术我认为债务=破产开发商。
2014年

3
答案对我来说很有意义,不知道为什么会有这么多缺点。引用迈克·科恩(Mike Cohn)的话:“单元测试应该是可靠的自动化测试策略的基础,因此代表着金字塔的最大部分。自动化的单元测试非常棒,因为它们将特定的数据提供给程序员,这是一个错误,而且还在不断发展。第47"行 mountaingoatsoftware.com/blog/...
guillaume31

4
@ guillaume31仅仅因为有人曾经说过他们好就意味着他们是好。以我的经验,单元测试未检测到错误,因为它们已预先更改为新要求。同样,大多数错误都是集成错误。因此,我通过集成测试检测了其中的大多数。
Yggdrasil 2014年

2
@Yggdrasil我还可以引用Martin Fowler的话:“如果您在高级测试中失败,不仅是您的功能代码中有错误,而且还缺少单元测试”。martinfowler.com/bliki/TestPyramid.html无论如何,如果仅集成测试对您有用,那就很好。我的经验是,尽管需要,但与单元测试相比,它们速度较慢,传递的故障消息更不准确,操作性(组合性更高)。另外,在编写集成测试时,我倾向于适应未来的情况-推理之前设想的场景,而不是对象本身的正确性。
guillaume31 2014年

2

TDD的“胜利”是,一旦编写了测试,就可以将它们自动化。不利的一面是,它可能消耗大量的开发时间。这是否真的减慢了整个过程的速度尚无定论。有观点认为,前期测试减少了在开发周期结束时要修复的错误数量。

这是BDD出现的地方,因为行为可以包含在单元测试中,因此根据定义,该过程不那么抽象,而且更具体。

显然,如果有无限长的时间可用,您将对各种品种进行尽可能多的测试。但是,时间通常有限,并且连续测试仅在一定程度上具有成本效益。

所有这些得出的结论是,提供最大价值的测试应该放在流程的最前面。这本身并不能自动使一种类型的测试胜于另一种类型的测试-更重要的是,每种情况都必须根据其优点进行评估。

如果您要编写供个人使用的命令行窗口小部件,则主要是对单元测试感兴趣。Web服务说需要大量的集成/行为测试。

尽管大多数类型的测试都集中在所谓的“竞赛线”上,即测试当今企业的需求,但是单元测试非常擅长清除可能在以后的开发阶段出现的细微错误。由于这是一项无法轻易衡量的优势,因此常常被忽略。


1
我预先编写了测试,并且编写了足够的测试来覆盖大多数错误。至于以后会出现的错误。在我看来,这是一项要求已更改或新的要求开始起作用。比集成/行为测试需要更改的更多。如果这样,在旧的要求中显示了一个错误,我对此的测试将显示它。至于自动化。我所有的测试一直在运行。
Yggdrasil 2014年

我在最后一段中想到的示例是说,一个库仅由单个应用程序使用,但是当时有一个业务需求,那就是将其设为通用库。在这种情况下,最好为您提供至少一些单元测试,而不是为您附加到该库的每个系统编写新的集成/行为测试。
罗比·迪

2
自动化测试和单元测试完全是正交的。任何自重的项目都将进行自动集成和功能测试。当然,您通常不会看到手动单元测试,但是它们可以存在(基本上,手动单元测试是针对某些特定功能的测试实用程序)。
Jan Hudec 2014年

的确如此。在发展领域之外存在相当长一段时间的自动化第三方工具市场蓬勃发展。
罗比·迪

1

单元测试的最后一个优点是它们更快。我已经写了足够多的集成测试,知道它们可以和单元测试一样快。

这是关键,而不仅仅是“最后的优势”。当项目变得越来越大时,您的集成验收测试变得越来越慢。在这里,我的意思是太慢了,您将停止执行它们。

当然,单元测试也变得越来越慢,但是它们仍然比数量级要快。例如,在我以前的项目(C ++,大约600 kLOC,4000个单元测试和200个集成测试)中,执行所有操作大约需要一分钟,而执行集成测试则需要15分钟以上。要为更改的零件构建并执行单元测试,平均只需不到30秒的时间。当您可以如此快地执行操作时,您将一直想要执行此操作。

只是为了清楚起见:我并不是说不添加集成和验收测试,但是看起来您以错误的方式进行了TDD / BDD。

单元测试也对设计有益。

是的,考虑可测试性的设计将使设计更好。

问题在于,超过一半的开发时间用于编写/重构测试。所以最后我不想实现任何其他功能,因为我需要重构并编写许多测试。

好吧,当需求更改时,您确实必须更改代码。如果您不编写单元测试,我会告诉您尚未完成工作。但这并不意味着您应该拥有100%的单元测试覆盖率-这不是目标。有些东西(例如GUI或访问文件等)甚至都不打算进行单元测试。

这样的结果是更好的代码质量和另一层测试。我会说这是值得的。


我们还进行了几千次验收测试,整个过程要花一周的时间。


1
你看过我的例子吗?这一直都在发生。关键是,当我实现新功能时,我更改/添加了单元测试,以便他们测试新功能-因此不会破坏任何单元测试。在大多数情况下,我会受到单元测试无法检测到的更改的副作用-因为环境是模拟的。根据我的经验,因为从未进行过任何单元测试,所以告诉我我已经破坏了现有功能。总是通过集成和验收测试向我展示了我的错误。
Yggdrasil 2014年

至于执行时间。随着应用程序的增长,我几乎拥有越来越多的隔离组件。如果没有,我做错了什么。当我实现一项新功能时,它几乎只包含在有限数量的组件中。我在整个应用程序范围内编写了一个或多个验收测试-这可能很慢。另外,我从组件的角度编写了相同的测试-这种测试是快速的,因为组件通常是快速的。我可以一直执行组件测试,因为它们足够快。
Yggdrasil 2014年

@Yggdrasil正如我说的,单元测试并非一帆风顺,但它们通常是测试的第一层,因为它们是最快的。其他测试也很有用,应合并使用。
BЈовић

1
仅仅因为它们更快并不意味着仅仅因为这个原因或因为编写它们是很普遍的方式就应该使用它们。就像我说的那样,我的单元测试不会中断-因此它们对我没有任何价值。
Yggdrasil

1
问题是,当单元测试不中断时,我具有什么价值?当我总是需要调整它们以适应新的要求时,为什么还要麻烦写它们呢?我从中看到的唯一价值是对算法和其他具有较高排列的类。但是这些少于组件和验收测试。
Yggdrasil 2014年
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.