您有一个X类,并且编写了一些用于验证行为X1的单元测试。
还有一个类A以X作为依赖项。

为A编写单元测试时,您会模拟X换句话说,当进行单元测试A时,您将X的模拟行为设置(假设)为X1。
随着时间的流逝,人们确实在使用您的系统,需要改变,X演变:您修改X以显示行为X2。显然,针对X的单元测试将失败,您需要对其进行调整。

但是A呢?修改X的行为后(由于X的模拟),针对A的单元测试不会失败。

我期望在使用“真实”(修改后的)X时检测到A的结果会有所不同吗?

我期望得到以下答案:“这不是单元测试的目的”,但是单元测试有什么价值呢?它真的只是告诉您,当所有测试通过时,您还没有引入重大更改吗?
当某个类的行为发生变化时(有意或无意),您如何检测(最好以自动化方式)所有结果呢?我们不应该更专注于集成测试吗?

评论

(为什么)可能重复,单元测试不测试依赖关系很重要?

除了建议的所有答案之外,我还必须对以下陈述表示怀疑:“它真的只告诉您,当所有测试通过时,您还没有进行重大改变吗?”如果您真的认为消除对重构的恐惧几乎没有价值,那么您就可以快速编写不可维护的代码

单元测试可以告诉您代码单元是否按预期运行。没有更多或更少。模拟和测试双打为您提供了一个人为的,受控的环境,可让您(单独)练习您的代码单元,以查看其是否符合您的期望。或多或少。

我相信您的前提是不正确的。当您提到X1时,您是说X实现了接口X1。如果将接口X1更改为X2,则在其他测试中使用的模拟程序将不再编译,因此也不得不修复这些测试。班级行为的变化不重要。实际上,您的A类不应该依赖于实现细节(在这种情况下,这就是您要更改的内容)。因此,对于A的单元测试仍然是正确的,并且它们告诉您A在给定接口的理想实现的情况下仍然有效。

我不了解您,但是当我不得不在没有测试的代码库中工作时,我会被吓死了,我会破坏一些东西。又为什么呢因为它发生的频率很高,以至于某些原本不想要的东西会破裂。祝福我们测试人员的内心,他们无法测试所有内容。甚至接近。但是在无聊的例程之后,单元测试会很高兴地从无聊的例程中消失。

#1 楼


为A编写单元测试时,您会模拟X


吗?除非绝对必要,否则我不会。我必须要:



X很慢,或者

X有副作用

如果这两个都不应用,然后我的A单元测试也将测试X。要做其他任何事情都会将测试隔离到一个不合逻辑的极端。

如果您的代码中有部分内容使用其他代码的模拟部分,那么我会同意:这种单元的意义是什么测试?所以不要这样做。让这些测试使用真正的依赖项,因为它们以这种方式构成了更有价值的测试。

如果某些人不满意您称这些测试为“单元测试”,则将其称为“自动化测试”并继续编写良好的自动化测试。

评论


@Laiv,不,单元测试应该作为一个单元,即与其他测试隔离运行。节点和图可以加息。如果我可以在短时间内运行一个隔离的,无副作用的免费端到端测试,那就是单元测试。如果您不喜欢该定义,请将其称为自动测试,并停止编写废话测试以适应愚蠢的语义。

– David Arno
18 Mar 27 '18 at 12:36

@DavidArno遗憾的是,隔离的定义非常广泛。有些人希望“单元”包括中间层和数据库。他们可以相信自己喜欢的任何东西,但是有可能在任何规模的开发中,轮子都会在相当短的时间内脱落,因为构建管理器会将其丢弃。通常,如果将它们隔离到程序集(或等效项)中,那很好。 N.B.如果您对接口进行编码,那么以后添加模拟和DI会容易得多。

–罗比·迪(Robbie Dee)
18 Mar 27 '18 at 12:47



您主张的是另一种测试,而不是回答问题。这是正确的一点,但这是一种相当不为人知的方法。

–菲尔·弗罗斯特(Phil Frost)
18 Mar 27 '18 at 23:53

@PhilFrost引述自己的话:“如果有些人不喜欢您将这些测试称为“单元测试”,那么就称它们为“自动化测试”,然后继续编写良好的自动化测试。”编写有用的测试,而不是仅仅满足某个单词随机定义的愚蠢测试。或者,也可以接受,也许您对“单元测试”的定义有误,并且因为错误而对模拟进行了过多使用。无论哪种方式,您都将获得更好的测试。

– David Arno
18-3-28的7:21

我和@DavidArno在一起。在观看了Ian Cooper的演讲后,我的测试策略发生了变化:vimeo.com/68375232。简而言之:不要测试课程。测试行为。您的测试应该不了解用于实现所需行为的内部类/方法;他们应该只知道您的API /库的公开外观,并且应该对其进行测试。如果测试知识太多,那么您正在测试实现细节,并且测试变得脆弱,与实现耦合,实际上只是您的脖子上的一个锚点。

– Richiban
18 Mar 28 '18 at 12:33

#2 楼

你们两个都需要。单元测试可以验证每个单元的行为,还可以进行一些集成测试以确保它们正确连接。仅依赖集成测试的问题是由所有单元之间的交互导致的组合爆炸。

假设您有A类,则需要10个单元测试才能完全覆盖所有路径。然后,您有了另一个B类,它也需要10个单元测试来覆盖代码可以通过的所有路径。现在,在您的应用程序中,您需要将A的输出馈送到B。现在您的代码可以采用从A的输入到B的输出的100条不同的路径。

使用单元测试,您可以只需要20个单元测试+ 1个集成测试就可以完全覆盖所有情况。

使用集成测试,您将需要100个测试来覆盖所有代码路径。

关于仅依靠集成测试的弊端的精彩视频JB Rainsberger集成测试是骗局HD

评论


我敢肯定,关于集成测试功效的问号与覆盖所有其他层的单元测试齐头并进并非偶然。

–罗比·迪(Robbie Dee)
18-3-27的13:00

是的,但是您的20个单元测试的任何地方都不需要模拟。如果您有10个涵盖所有A的A测试和10个涵盖所有B的测试,并且还重新测试了25%的A作为奖励,那么这似乎是“好”和好事。在Bs测试中嘲笑A似乎很愚蠢(除非A确实存在问题的原因,例如A是数据库或带来大量其他问题)

–Richard Tingle
18年3月27日在21:22

我不同意这样一个想法,即如果您想全面覆盖,则单个集成测试就足够了。 B对A输出的反应将根据输出而变化;如果在A中更改参数会更改其输出,则B可能无法正确处理它。

– Matthieu M.
18 Mar 28 '18 at 9:09

@ Eternal21:我的意思是有时候问题不在于个人行为,而在于意想不到的互动。也就是说,在某些情况下,当A和B之间的粘连行为异常时。因此,A和B都根据规范进行操作,并且情况令人满意,但在某些输入上,粘合代码中存在错误……

– Matthieu M.
18 Mar 28 '18在11:53

@MatthieuM。我认为这超出了单元测试的范围。胶水代码本身可以进行单元测试,而通过胶水代码进行的A和B之间的交互是集成测试。当发现特定的边缘情况或错误时,可以将其添加到粘合代码单元测试中,并最终在集成测试中进行验证。

– Andrew T Finnell
18年4月1日在20:25

#3 楼


在编写A的单元测试时,您将模拟X。换句话说,在对A进行单元测试时,您将(假设)X的模拟行为设置为X1。时间流逝,人们确实在使用您的系统,需要改变,X演变:您修改X以显示行为X2。显然,针对X的单元测试将失败,您需要对其进行调整。


哇,等等。 X失败的测试含义非常重要,以至于无法掩盖。

如果将X的实现从X1更改为X2会破坏X的单元测试,则表明您已经做了一个在Liskov的意义上,X2不是X的向后不兼容的更改。

X2不是X,因此您应该考虑满足利益相关者需求的其他方式(例如引入新的规范) Y,由X2实现)。

有关更深入的见解,请参见Pieter Hinjens:软件版本的结尾或Rich Hickey轻松实现。

从A,前提是合作者遵守X合同。并且您的观察有效地是,对A进行隔离测试并不能保证A会识别违反X合同的合作者。

审查集成测试是一个骗局;在较高的层次上,期望您有尽可能多的隔离测试来确保X2正确地实现合同X,并且有尽可能多的隔离测试来确保A给出正确的响应(从X发出有趣的响应),并且一些较小的集成测试,以确保X2和A同意X的含义。

有时您会看到这种区别表示为孤立测试与sociable测试;请参阅Jay Fields有效地进行单元测试。


我们不应该更专注于集成测试吗?


再次,看到集成测试是一个骗局-Rainsberger详细描述了一个正反馈循环(根据他的经验)对于依赖于集成(注释拼写)测试的项目很常见。总而言之,如果没有孤立/单独的测试对设计施加压力,质量就会下降,从而导致更多的错误和更多的集成测试...。

您还将需要(一些)集成测试。除了由多个模块引入的复杂性之外,执行这些测试还比隔离测试带来更多的拖累。在工作进行时非常快速地进行检查,这样可以提高效率,而在您认为自己“完成”时可以保存其他检查。

评论


这应该是公认的答案。该问题概述了一种情况,其中类的行为已以不兼容的方式进行了修改,但在外观上仍然相同。这里的问题在于应用程序的设计,而不是单元测试。在测试中遇到这种情况的方法是在这两个类之间使用集成测试。

–尼克·科德(Nick Coad)
18-3-29的3:30

“如果没有孤立的/单独的测试对设计施加压力,质量就会下降”。我认为这是重要的一点。除行为检查外,单元测试还会产生副作用,迫使您进行更具模块化的设计。

–MickaëlG
18 Mar 30 '18 at 12:07

我想这都是真的,但是如果外部依赖对合同X引入了向后不兼容的更改,这对我有什么帮助?也许库中I / O执行类破坏了兼容性,我们之所以嘲笑X,是因为我们不希望CI中的单元测试依赖大量I / O。我认为OP正在要求对此进行测试,但我不知道这如何回答问题。如何对此进行测试?

– Gerrit
19年8月23日在15:21

#4 楼

首先,我要说的是问题的核心前提是有缺陷的。

您从不测试(或模拟)实现,而是在测试(和模拟)接口。

如果我有一个实现接口X1的真实类X,那么我可以编写一个也符合X1的模拟XM。然后,我的A类必须使用实现X1的东西,它可以是X类,也可以是模拟XM。

现在,假设我们更改X来实现新的接口X2。好吧,显然我的代码不再编译。 A需要实现X1的东西,并且不再存在。问题已得到确定并且可以解决。

假设我们不用修改X1,而是对其进行修改。现在,所有班级都准备好了。但是,模拟XM不再实现接口X1。问题已得到识别,可以解决。


单元测试和模拟的整个基础是编写使用接口的代码。接口的使用者不关心代码的实现方式,只关心遵守相同的协定(输入/输出)。

当您的方法有副作用时,这种方法会分解,但是我认为可以安全排除为“无法进行单元测试或模拟”的内容。

评论


这个答案提出了许多不需要成立的假设。首先,它粗略地假定我们使用C#或Java(或更准确地说,我们使用的是编译语言,该语言具有接口,并且X实现了一个接口;这些都不是必须的)。其次,它假定对X的行为或“合同”进行任何更改都需要对X实现的接口(如编译器所理解的)进行更改。即使我们使用Java或C#,这也不是真的。您可以更改方法实现而不更改其签名。

–马克·阿默里(Mark Amery)
18 Mar 28 '18 at 12:33

@MarkAmery的确,“接口”术语更特定于C#或Java,但是我认为,要点是假设行为已定义为“合同”(如果未进行编码,则无法自动检测到此情况)。您也完全正确,可以在不更改合同的情况下更改实现。但是,在不更改接口(或合同)的情况下更改实现不应影响任何消费者。如果A的行为取决于接口(或协定)的实现方式,则不可能(有意义地)进行单元测试。

–Vlad274
18 Mar 28 '18 at 12:47

“您也完全正确,可以在不更改合同的情况下更改实现”-同样,这不是我要提出的重点。相反,我要在合同(程序员对对象应该做的理解,可能在文档中指定)和接口(方法签名列表,编译器可以理解)之间进行区分,并说合同可以无需更改界面即可进行更改。从类型系统的角度来看,具有相同签名的所有功能都是可以互换的,但实际上不能互换!

–马克·阿默里(Mark Amery)
18-3-28在12:55



@MarkAmery:我不认为Vlad在使用“接口”一词时所使用的含义与您在使用它时所使用的含义相同;在我阅读答案的方式中,它不是在狭义的C#/ Java意义上(即一组方法签名)谈论接口,而是在一般意义上(例如)使用。在术语“应用程序编程接口”或什至“用户界面”中。 [...]

–伊尔马里·卡洛宁(Ilmari Karonen)
18-3-29在14:33



@IlmariKaronen如果Vlad使用“接口”来表示“合同”,而不是狭义的C#/ Java,则声明“现在,假设我们更改X来实现新接口X2。那么,显然我的代码不再编译了。 ”完全是错误的,因为您可以更改合同而无需更改任何方法签名。但老实说,我认为这里的问题是Vlad并没有始终使用这两种含义,而是对其进行了混淆-这就是导致声称X1合同的任何变更必然会导致编译错误而没有注意到这是错误的的原因。 。

–马克·阿默里(Mark Amery)
18-3-29在14:42



#5 楼

依次回答您的问题:


单元测试具有什么价值?


编写和运行它们很便宜,并且您会得到早期反馈。如果您破坏X,只要测试良好,就会立即发现或多或少。除非您已经对所有的层进行了单元测试(甚至是在数据库上),否则甚至不要考虑编写集成测试。


真的只告诉您所有测试通过时,您还没有
引入了突破性的变化


通过测试确实可以告诉您很少。您可能没有编写足够的测试。您可能没有测试足够的方案。代码覆盖率可以在这里有所帮助,但这不是灵丹妙药。您可能有始终通过的测试。因此,红色是经常被忽略的红色,绿色,重构的第一步。


当某个类的行为发生变化时(有意或无意),
如何检测(最好是在自动化的方式)所有的后果


更多的测试-尽管工具越来越好。但是您应该在接口中定义类行为(请参见下文)。 N.B.在测试金字塔上总会有一个手动测试的地方。


我们不应该更多地关注集成测试吗?


更多集成测试也不是答案,它们的编写,运行和维护成本很高。根据您的构建设置,您的构建管理器可能仍会排除它们,从而使它们依赖于开发人员记住(从来都不是一件好事!)。如果他们有良好的单元测试,他们会在五分钟内发现。如果失败,请尝试仅运行软件-这是您的最终用户将关心的所有事情。用户运行整个套件时,如果整个纸牌屋掉下来,没有百万个单元测试通过的要点。

如果要确保类A以相同的方式使用类X,则应使用接口而不是构造。这样一来,在编译时就更有可能发生重大更改。

#6 楼

没错

单元测试可以测试一个单元的隔离功能,它可以一目了然地检查它是否按预期工作并且不包含愚蠢的错误。

单元测试是不能测试整个应用程序是否正常工作。

很多人忘记的是,单元测试只是验证代码的最快,最肮脏的方法。一旦知道您的小例程正常工作,那么您还必须运行集成测试。单元测试本身仅比没有测试好一点。

我们之所以拥有单元测试,是因为它们应该便宜。快速创建,运行和维护。一旦开始将它们转换为最小集成测试,您将陷入痛苦的世界。您最好进行完整的集成测试,如果要执行此操作,则完全忽略单元测试。

现在,有些人认为单元不仅仅是一个类中的函数,而是全班本身(包括我自己)。但是,所有这一切都是增加单元的大小,因此您可能需要较少的集成测试,但仍然需要它。如果没有完整的集成测试套件,仍然无法验证您的程序是否应该执行预期的操作。

,然后,您仍然需要实时(或半运行)运行完整的集成测试-live)系统,以检查其是否符合客户使用的条件。

#7 楼

单元测试不能证明任何东西的正确性。对于所有测试都是如此。如果需要定期验证正确性,通常将单元测试与基于合同的设计(按合同设计是另一种说法)结合使用,并可能自动进行正确性证明。

由类不变式,前提条件和后置条件组成的合同,可以通过将较高级别的组件的正确性基于较低级别的组件的契约来分层证明正确性。这是合同设计背后的基本概念。

评论


单元测试不能证明任何东西的正确性。不确定我是否理解这一点,单元测试肯定会检查自己的结果吗?还是您是说某个行为无法证明是正确的,因为它可能包含多个层次?

–罗比·迪(Robbie Dee)
18年3月28日在17:56

@RobbieDee我猜,他的意思是,当您测试fac(5)== 120时,您尚未证明fac()确实返回了其参数的阶乘。您仅证明了当您通过5时,fac()返回的阶乘为5。即使这样也不确定,因为fac()可能会在廷巴克图全月食后的第一个星期一返回42,而不是……。这是因为您不能通过检查各个测试输入来证明是否符合要求,您需要检查所有可能的输入,还需要证明您没有忘记任何输入(例如读取系统时钟)。

–cmaster-恢复莫妮卡
18年3月29日在10:08

对于真正的目标,@ RobbieDee测试(包括单元测试)是一个较差的替代品(通常是最佳的替代品),它是机器检查的证明。考虑被测单元的整个状态空间,包括其中任何组件或模型的状态空间。除非您的状态空间非常有限,否则测试无法覆盖该状态空间。完全覆盖将是一个证明,但这仅适用于微小的状态空间,例如,测试包含单个可变字节或16位整数的单个对象。自动证明更有价值。

–弗兰克·希勒曼
18 Mar 29 '18 at 15:56

@cmaster您很好地总结了测试和证明之间的区别。谢谢!

–弗兰克·希勒曼
18 Mar 29 '18在15:57

#8 楼

我发现进行大量模拟的测试很少有用。大多数时候,我最终会重新实现原始类已经具有的行为,这完全违背了模拟的目的。

M.e.更好的策略是将关注点很好地分开(例如,您可以测试应用的A部分,而无需引入B部分到Z部分)。这样一个好的架构确实有助于编写好的测试。

此外,只要愿意将副作用降低,例如,我愿意接受它们。如果我的方法修改了数据库中的数据,那就去吧!只要我可以将数据库回滚到以前的状态,会有什么害处?另外,我的测试还可以检查数据是否符合预期,这是有好处的。内存数据库或数据库的特定测试版本在这里确实有帮助(例如RavenDB的内存测试版本)。

最后,我想对服务边界进行模拟,例如不要对该服务b进行http调用,但让我们拦截它并引入适当的

#9 楼

我希望两个阵营的人都理解类测试和行为测试不是正交的。

类测试和单元测试可以互换使用,也许不应该这样。一些单元测试恰好在类中实现。就这些。单元测试已经在没有类的语言中进行了数十年。

关于测试行为,完全可以使用GWT构造在类测试中进行。

您的自动化测试会按照课程或行为进行,而不是取决于您的优先级。有些将需要快速进行原型制作并拿出一些东西,而另一些则将由于房屋风格而受到覆盖范围的限制。很多原因。它们都是完全有效的方法。您付钱,您可以选择。

因此,代码中断时该怎么办。如果已将其编码为接口,则只需更改具体内容(以及任何测试)。

但是,引入新的行为根本不会损害系统。 Linux等具有不推荐使用的功能。而且诸如构造函数(和方法)之类的东西可以很高兴地被重载,而不必强迫所有调用代码进行更改。 (由于时间限制,复杂性或其他原因)。如果具有全面的测试,那么上课就容易得多。

评论


如果您有具体的看法,我将进行书面实施。这是从另一种语言(我猜是法语)算出来的,还是固结和实现之间的重要区别?

– Peter Taylor
18 Mar 29 '18 at 11:44

#10 楼

除非X的接口发生更改,否则您无需更改A的单元测试,因为与A相关的任何内容都没有更改。听起来好像您真的一起写了X和A的单元测试,但是称它为A的单元测试:


当您为A编写单元测试时,您在模拟X。换句话说,在进行单元测试A时,您将(假设)X的模拟行为设置为X1。


理想情况下,X的模拟应该模拟X的所有可能行为,而不仅仅是您期望从X进行的行为。因此,无论您实际上在X中实现什么,A都应该已经能够处理它。因此,对X的更改(除了更改接口本身之外)不会对A的单元测试产生任何影响。

例如:假设A是一种排序算法,而A提供了要排序的数据。 X的模拟应提供一个空返回值,一个空数据列表,一个元素,多个元素已排序,多个元素尚未排序,多个元素向后排序,具有相同元素的列表重复,空值混杂在一起,可笑

所以X最初在星期一返回排序后的数据,在星期二返回空列表。但是现在,当X在星期一返回未排序的数据并在星期二抛出异常时,A不在乎-A的单元测试中已经涵盖了这些情况。

评论


如果X刚将返回数组中的索引从foobar重命名为foobarRandomNumber怎么办,该如何计算呢?如果您明白我的意思,那基本上是我的问题,我将返回的列从secondName重命名为姓,这是一个经典任务,但是我的测试永远不会知道,因为它被嘲笑了。我只是有一种奇怪的感觉,好像这个问题中的许多人在评论之前从未真正尝试过这样的事情

– FantomX1
19-10-6在22:25



编译器应该已经检测到此更改,并给了您一个编译器错误。如果您使用Javascript之类的东西,那么我建议您切换到Typescript或使用Babel这样的编译器来检测这些东西。

– Moby磁盘
19-10-13在15:07

如果我在PHP或Java或Javascript中使用数组,如果更改数组索引或将其删除,那么这些语言中的任何一种都不会由编译器告诉您,该索引可能嵌套在第36位-考虑周到的数字数组的嵌套级别,因此我认为编译器不是解决方案。

– FantomX1
19-10-13在17:28

#11 楼

您必须查看不同的测试。

单元测试本身只会测试X。它们在那里是为了防止您更改X的行为,但不能保护整个系统。他们确保您可以在不引入行为改变的情况下重构您的课堂。而且,如果您破坏了X,那么您就破坏了它...

A确实应该对X进行单元测试,并且即使更改了Mock,它的测试也应保持通过。

但是测试不止一个级别!也有集成测试。这些测试可以验证类之间的交互。这些测试通常价格较高,因为它们并未对所有内容都使用模拟。例如,集成测试实际上可能会将记录写入数据库中,而单元测试应该没有外部依赖性。

如果X需要具有新的行为,最好提供一个新的提供所需结果的方法