模拟/存根以避免测试依赖性增加了测试的复杂性。它在生产代码中增加了人工灵活性/去耦要求,以支持模拟。 (我不同意说这会促进良好设计的任何人。写额外的代码,引入依赖注入框架之类的东西,或者以其他方式增加代码库的复杂性以在没有实际用例的情况下使事情变得更灵活/可插拔/可扩展/解耦是过度设计,而不是其次,测试依赖项意味着用在输入之外的所有关键低级代码都经过输入的测试,而不是那些编写测试的人明确想到的。通过在高级功能上运行单元测试而不嘲笑它所依赖的低级功能,我发现了低级功能中的许多错误。理想情况下,这些可以通过单元测试中的低级功能找到,但是总是会漏掉一些案例。
这又是什么呢?单元测试不要同时测试其依赖关系真的很重要吗?如果是,为什么?
编辑:我可以理解嘲笑外部依赖关系(如数据库,网络,Web服务等)的价值。(感谢Anna Lear激励我澄清这一点。)到内部依赖项,即没有任何直接外部依赖项的其他类,静态函数等。
#1 楼
这是一个定义问题。具有依赖项的测试是集成测试,而不是单元测试。您还应该具有集成测试套件。区别在于集成测试套件可能在不同的测试框架中运行,并且可能不作为构建的一部分,因为它们花费的时间更长。对于我们的产品:
我们的单元测试在运行每次构建都需要几秒钟。
每次签入都会运行我们的集成测试的子集,需要10分钟。
评论
不要忘记使用回归测试来覆盖发现的和已修复的错误,以防止在将来的维护期间将其重新引入系统。
– RBerteig
2011年4月6日在7:15
即使我同意这个定义,我也不会坚持。某些代码可能具有可接受的依赖项,例如StringFormatter,并且大多数单元测试仍将其考虑在内。
–约翰勒蒙
2013年9月25日在7:06
danip:明确的定义很重要,我会坚持使用。但是,认识到这些定义是目标也很重要。他们值得针对,但您并不总是需要靶心。单元测试通常会依赖于较低级别的库。
–杰弗里·浮士德(Jeffrey Faust)
2013年12月3日13:54
了解外观测试的概念也很重要,它不测试组件如何组装在一起(例如集成测试),而是单独测试单个组件。 (即对象图)
–里卡多·罗德里格斯(Ricardo Rodrigues)
2014年9月24日16:10
它不仅要更快,而且要非常繁琐或难以管理,想象一下,如果不这样做,您的模拟执行树可能会成倍增长。想象一下,如果您进一步推动模拟,可以在您的单元中模拟10个依赖关系,假设10个依赖关系中的1个在许多地方使用,并且它有20个自己的依赖关系,因此您必须模拟+ 20个其他依赖关系,可能在许多地方重复。在最后的api点,您必须进行模拟-例如数据库,这样就不必重置它,并且它告诉您的速度更快
– FantomX1
19-10-6在22:49
#2 楼
适当放置所有依赖项的测试仍然很重要,但是正如Jeffrey Faust所说的那样,它更在集成测试领域。单元测试最重要的方面之一就是使您的测试值得信赖。如果您不相信通过测试确实意味着一切都很好,而失败的测试确实意味着生产代码中存在问题,那么您的测试就不会像它们可能有用的那样。
为了使您的测试值得信赖,您必须做一些事情,但是我将只关注其中一项。您必须确保它们易于运行,以便所有开发人员都可以在签入代码之前轻松地运行它们。“易于运行”意味着您的测试可以快速运行,并且不需要进行大量配置或设置即可使它们运行。理想情况下,任何人都应该能够签出最新版本的代码,立即运行测试,然后查看它们是否通过。
抽象出对其他事物(文件系统,数据库,Web服务,等)使您避免进行配置,并使您和其他开发人员更容易受到以下情况的影响:“哦,测试失败,因为我没有设置网络共享。哦,好的。我将运行他们以后。”
如果要测试对某些数据的处理方式,则针对该业务逻辑代码的单元测试不必关心如何获取这些数据。能够测试应用程序的核心逻辑而无需依赖诸如数据库之类的东西真是太棒了。如果您不这样做,那就会错过。
P.S.我还要补充一点,绝对有可能以可测试性为名进行过度设计。对应用程序进行测试驱动有助于缓解这种情况。但是无论如何,糟糕的方法实现并不会降低方法的有效性。如果有人不停地问“我为什么要这样做?”,任何事情都可能被滥用和过度。在开发时。
就内部依赖性而言,事情变得有些混乱。我想考虑的方式是,我想尽可能地保护我的班级,以免因错误的原因而改变。如果我具有类似这样的设置...
public class MyClass
{
private SomeClass someClass;
public MyClass()
{
someClass = new SomeClass();
}
// use someClass in some way
}
我通常不在乎
SomeClass
的创建方式。我只想使用它。如果SomeClass发生更改,并且现在需要构造函数使用参数...那不是我的问题。我不必更改MyClass来适应这一点。 现在,这仅涉及设计部分。就单元测试而言,我还想保护自己不受其他课程的影响。如果我正在测试MyClass,我希望知道没有外部依赖的事实,那就是SomeClass在某些时候没有引入数据库连接或其他外部链接。
但是,更大的问题是我也知道我的某些方法的结果依赖于SomeClass上某个方法的输出。如果不对SomeClass进行模拟/存根,则可能无法更改需求中的输入。如果幸运的话,我可以在测试中组合环境,这样可以触发SomeClass的正确响应,但是这样做会给测试带来复杂性并使它们变脆。
重写MyClass以在构造函数中接受SomeClass的实例使我能够创建一个SomeClass的假实例,该实例返回我想要的值(通过模拟框架或手动模拟)。在这种情况下,我通常不必引入接口。在许多方面,是否进行此操作是个人选择,具体取决于您选择的语言(例如,在C#中更可能使用接口,但在Ruby中绝对不需要)。
评论
+1很好的答案,因为外部依赖性是我写原始问题时没有想到的一个案例。我已经回答了我的问题。查看最新编辑。
–dsimcha
2011年4月6日,下午1:27
@dsimcha我扩展了我的答案,以进一步详细说明。希望能帮助到你。
–亚当·李尔♦
2011年4月6日,下午1:41
亚当,您能建议一种语言“如果SomeClass更改了,现在需要构造函数的参数...我不必更改MyClass”是真的吗?抱歉,这是我的一个不寻常的要求。我看到不必更改MyClass的单元测试,而不必更改MyClass ...哇。
–́Мסž
2011年4月6日4:34
@moz我的意思是,如果创建SomeClass的方式,则将MyClass注入其中而不是在内部创建,则无需更改MyClass。通常情况下,MyClass不需要关心SomeClass的设置详细信息。如果SomeClass的界面发生了变化,那么是的...如果MyClass使用的任何方法受到影响,仍然必须对其进行修改。
–亚当·李尔♦
2011年4月6日下午4:37
如果在测试MyClass时嘲笑SomeClass,如何检测MyClass是否使用了SomeClass错误或依赖于未经测试的SomeClass怪异(可能会改变)?
–名
17年1月20日在23:56
#3 楼
除了单元vs集成测试问题之外,请考虑以下内容。Class Widget依赖于类Thingamajig和WhatsIt。
问题出在哪一类中?依赖项。
评论
@Brook:如何查看所有Widget依赖项的结果?如果它们全部通过,那就是Widget的问题,除非另行证明。
–dsimcha
2011年4月6日在3:55
@dsimcha,但现在您要在游戏中添加复杂性以检查中间步骤。为什么不简化而仅先进行简单的单元测试?然后进行集成测试。
– asoundmove
2011年4月6日下午5:19
@dsimcha,这听起来很合理,直到您进入一个非平凡的依赖关系图为止。假设您有一个复杂的对象,该对象具有3+层以上的依赖关系,它将变成O(N ^ 2)搜索问题,而不是O(1)
–布鲁克
2011年4月6日上午11:52
另外,我对SRP的解释是,一个类在概念/问题域级别应该承担单一责任。在较低的抽象层次上,履行此职责是否要求它执行许多不同的事情并不重要。如果将SRP放到极限,则SRP将与OO相反,因为大多数类都保存数据并对其执行操作(以足够低的级别查看时有两件事)。
–dsimcha
2011年4月6日在17:29
@布鲁克:更多类型本身并不会增加复杂性。如果这些类型在问题域中具有概念上的意义,并使代码更易于理解(而不是更难理解),则可以使用更多类型。问题是人为地将在概念上耦合到问题域级别的事物去耦(即,您只有一个实现,可能永远不会有多个实现,等等),并且去耦不能很好地映射到问题域概念。在这些情况下,围绕此实现创建严肃的抽象是愚蠢,官僚和冗长的。
–dsimcha
2011年4月6日在17:42
#4 楼
想象一下编程就像烹饪。然后进行单元测试就像确保您的食材新鲜,美味等一样。而集成测试就像确保您的饭菜美味。最终,确保您的饭菜美味(您的系统正常运行)是最重要的,也是最终目标。但是,如果您的原料(单位)有效,那么您将可以找到更便宜的方法。
实际上,如果您可以保证自己的单位/方法有效,那么您更有可能拥有功能系统。我强调“更有可能”,而不是“某些”。您仍然需要进行集成测试,就像您仍需要有人品尝您烹制的饭菜并告诉您最终产品很好一样。有了新鲜的食材,您将更轻松地到达那里。
评论
并且当您的集成测试失败时,单元测试可能会解决该问题。
–蒂姆·威利斯克罗夫特(Tim Williscroft)
2011年4月6日在4:30
举个比喻:当您发现餐食很糟糕时,可能需要进行一些调查才能确定这是因为您的面包发霉了。但是,结果是您添加了一个单元测试,以在烹饪前检查发霉的面包,这样就不会再次发生该特定问题。然后,如果以后又有进餐失败,则可以消除发霉面包的原因。
– Kyralessa
11年3月3日在16:24
喜欢这个比喻!
–咆哮者
2014年5月17日10:00
#5 楼
通过完全隔离测试单元,可以测试该单元可以提交的所有数据变体和情况。由于它与其余部分隔离,因此您可以忽略对测试对象没有直接影响的变化。这反过来会大大降低测试的复杂性。集成了各种级别的依赖项的测试将使您可以测试单元测试中可能未测试的特定方案。
两者都很重要。仅进行单元测试,集成组件时就会不可避免地出现更复杂的细微错误。仅进行集成测试就意味着您在测试系统时没有信心对机器的各个部分进行测试。认为仅通过进行集成测试就可以实现更好的完整测试几乎是不可能的,因为添加的组件越多,输入组合的数量就变得非常快(请考虑阶乘),并且创建具有足够覆盖范围的测试变得非常快。
因此,简而言之,我通常在几乎所有项目中都使用三个级别的“单元测试”:
单元测试,通过模拟或存根依赖性进行隔离以测试地狱单个组件。理想的情况是尝试完全覆盖。
集成测试可以测试更细微的错误。精心设计的抽查会显示极限情况和典型情况。通常不可能完全覆盖,将精力集中在使系统发生故障的原因上。
规格测试将各种实际数据注入系统中,对数据进行检测(如果可能)并观察输出以确保其符合要求。规范和业务规则。
依赖注入是实现此目的的一种非常有效的方法,因为它允许非常容易地隔离用于单元测试的组件,而不会增加系统的复杂性。您的业务场景可能不保证使用注入机制,但是您的测试场景几乎可以保证。对我而言,这足以使之成为必不可少的。您也可以使用它们通过部分集成测试来独立测试不同的抽象级别。
#6 楼
这又是什么呢?单元测试不要同时测试其依赖关系真的很重要吗?如果是这样,为什么?
单元。表示单数。
测试2个事物意味着您拥有这两个事物以及所有功能依赖性。
如果添加第3个事物,则将增加测试中的功能依赖性超越线性。事物之间的互连增长快于事物的数量。
被测试的n项之间存在n(n-1)/ 2个潜在依存关系。
这是主要原因。
简单具有价值。
#7 楼
还记得您第一次学习递归的方法吗?我的教授说:“假设您有一种方法可以计算x”(例如,为任何x求解fibbonacci)。 “要解决x,必须为x-1和x-2调用该方法”。同样,将依赖项存根也可以使它们假装存在并测试当前单元是否应执行的工作。当然,前提是您要严格测试依赖关系。甚至针对测试只关注单一责任,消除了您必须要做的心理杂耍。#8 楼
(这是次要的答案。感谢@TimWilliscroft的提示。)如果出现以下情况,故障更容易定位:
并且仅当该测试所涵盖的代码中存在错误时,每个测试才会失败。但是,如OP的说明所示(依赖关系是错误的),如果不对依赖关系进行测试,将很难查明故障的位置。
评论
为什么不对依赖项进行测试?它们大概是较低的级别,并且更容易进行单元测试。此外,借助涵盖特定代码的单元测试,可以更轻松地确定故障的位置。
– David Thornley
2011年4月6日在16:02
#9 楼
很多好的答案。我还要添加其他几点:单元测试还允许您在不存在依赖项时测试代码。例如。您或您的团队尚未编写其他层,或者您正在等待另一家公司提供的接口。
单元测试还意味着您不必具有完整的环境在您的开发机器上(例如数据库,Web服务器等)。我强烈建议所有开发人员都拥有这样的环境,但是为了减少bug等而将其削减了。但是,如果由于某种原因无法模仿生产环境,则单元测试至少可以给您一定的水平在进入更大的测试系统之前,对您的代码充满信心。
#10 楼
关于设计方面:我认为,即使小型项目也可以从使代码可测试中受益。您不一定必须引入类似Guice的东西(通常会做一个简单的工厂类),但是将构造过程与编程逻辑分开可以清楚地记录每个组件的依赖性通过界面进行分类(对团队中的新手非常有帮助)这些分类变得更加清晰和易于维护(一旦将丑陋的对象图创建放入单独的分类中)使更改变得更加容易)
评论
方法:是的,但是您必须添加额外的代码和额外的类才能执行此操作。代码应尽可能简洁,同时仍可读。恕我直言,添加工厂,而不是过度工程,除非您可以证明自己需要或很有可能需要它提供的灵活性。
–dsimcha
2011年4月6日13:05
#11 楼
嗯...在这些答案中有关于单元测试和集成测试的要点!我错过了这里有关成本和实用的观点。
那我很清楚看到了好处非常孤立的/原子的单元测试(可能彼此高度独立,并且可以选择并行运行它们,而没有任何依赖性,例如数据库,文件系统等)和(更高级别)集成测试,但是...这也是一个成本(时间,金钱,...)和风险的问题。
因此,在考虑“如何测试”之前,还有其他因素(例如“要测试的东西”)更为重要。 “根据我的经验...
我的客户是否(隐含地)为编写和维护测试的额外费用付费?
是一种更具测试驱动力的方法(在编写代码之前先编写测试)代码)在我的环境中确实具有成本效益(代码失败风险/成本分析,人员,设计规范,设置测试环境)?对生产使用情况进行测试(在最坏的情况下!)?
还取决于您的代码质量(标准)或框架,IDE,设计原则等。您和您团队关注以及他们的经验。编写良好,易于理解,足够好的文档化(理想情况下是自我文档化)的模块化...引入的错误可能比反之少。因此,进行大规模测试的真正“需求”,压力或总体维护/错误修复成本/风险可能不高。
让我们团队中的一位同事建议,我们将其推向了极致。尝试对我们的纯Java EE模型层代码进行单元测试,以期对数据库中的所有类进行100%的覆盖率,并模拟数据库。现实世界中的用例和Web ui工作流,因为我们不想冒任何用例失败的风险。
但是我们有大约100万欧元的紧预算,有相当紧的计划来编写所有代码。在客户环境中,潜在的应用程序错误对人类或公司而言并不是很大的危险。我们的应用将通过(某些)重要的单元测试,集成测试,具有设计的测试计划的关键客户测试,测试阶段等进行内部测试。我们不为某些核工厂或制药生产开发应用!!
(我们只有一个指定的数据库,可以轻松地为每个开发人员测试克隆,并与Webapp或模型层紧密耦合。但是我经常从上至下的方法(集成测试)进行操作,并尝试找到要点,以便为重要的测试(通常在模型层)进行“应用层削减”。 (因为有关“层”的信息很多)
此外,单元和集成测试代码不会对时间,金钱,维护,编码等产生负面影响。它很酷,应该应用但是要谨慎考虑并考虑在许多已开发的测试代码开始或运行5年后所产生的影响。就像在现实生活中,您不可能也不想使用很多或100%的安全机制来奔波。
孩子可以而且应该爬上某个地方,可能跌倒并伤到自己。
由于我加错了燃油(无效的输入:),汽车可能会停止工作。
如果时间按钮在3年后停止工作,烤面包可能会被烧掉。永远不要想在高速公路上开车并把我分离的方向盘握在手中:)
评论
“工程过度,设计不好”。您将不得不提供更多的证据。很多人不会称其为“过度工程”,而是“最佳实践”。@ S.Lott:当然这是主观的。我只是不想一堆答案来回避这个问题,并说模拟是好的,因为使代码可模拟可促进良好的设计。但是,总的来说,我讨厌处理以当前或可预见的将来没有明显好处的方式分离的代码。如果您现在没有多个实现,并且不希望在可预见的将来实现它们,那么恕我直言,您应该对其进行硬编码。它更简单,并且不会让客户端负担对象依赖项的详细信息。
不过,总的来说,我讨厌处理代码,除了设计不佳之外,这些代码的耦合没有其他基本原理。它更简单,不会给客户端带来孤立测试的灵活性。
@ S.Lott:澄清:我的意思是代码在没有任何明确用例的情况下就明显脱离了事物的耦合,特别是在使代码或其客户端代码更加冗长,引入了另一个类/接口等的情况下。当然,我并不是在要求代码比最简单,最简洁的设计紧密结合。同样,当过早创建抽象行时,它们通常会在错误的地方结束。
考虑更新您的问题,以澄清您所做的任何区别。整合评论序列是困难的。请更新问题以澄清和重点关注。