(为什么)单元测试不测试依赖项很重要?


103

我了解自动测试的价值,并在问题足够明确的地方使用它,以便我可以提出好的测试用例。但是,我注意到,这里和StackOverflow上的某些人强调测试一个单元,而不是测试其依赖项。在这里我看不到好处。

为避免测试依赖性而进行的模拟/存根增加了测试的复杂性。它在生产代码中增加了人工灵活性/去耦要求,以支持模拟。(我不同意说这会促进良好设计的任何人。写额外的代码,引入诸如依赖注入框架之类的东西,或者以其他方式增加代码库的复杂性以在没有实际用例的情况下使事情变得更灵活/可插拔/可扩展/解耦是过度设计,而不是好的设计。)

其次,测试依赖项意味着使用其他输入的测试关键的底层代码,而不是那些编写测试的人明确想到的输入。通过在高级功能上运行单元测试而不嘲笑它所依赖的低级功能,我发现了低级功能中的许多错误。理想情况下,这些可以通过单元测试中的低级功能找到,但是总是会漏掉一些情况。

另一面是什么?单元测试不要同时测试其依赖关系真的很重要吗?如果是这样,为什么?

编辑:我可以理解模拟外部依赖项(如数据库,网络,Web服务等)的价值。(感谢Anna Lear激励我进行澄清。)我指的是内部依赖项,即其他类,静态函数等。没有任何直接的外部依赖关系。


14
“工程过度,设计不好”。您将不得不提供更多的证据。很多人不会称其为“过度工程”,而是“最佳实践”。
S.Lott

9
@ S.Lott:当然这是主观的。我只是不想一堆答案来回避这个问题,并说模拟是好的,因为使代码可模拟可促进良好的设计。不过,总的来说,我讨厌处理以当前或可预见的未来没有明显好处的方式分离的代码。如果您现在没有多种实现,并且在可预见的将来不期望它们实现,那么恕我直言,您应该对其进行硬编码。它更简单,并且不会使对象负担对象依赖项的细节。
dsimcha 2011年

5
总的来说,尽管如此,我讨厌处理代码,除了设计不佳之外,这些代码的耦合没有其他基本原理。它更简单,不会给客户带来孤立测试的灵活性。
S.Lott

3
@ S.Lott:澄清:我的意思是代码在没有任何明确用例的情况下就明显地脱离了事物的耦合,特别是在使代码或其客户端代码更加冗长,引入了另一个类/接口等的情况下。当然,我并不是在要求代码比最简单,最简洁的设计紧密结合。同样,当您过早地创建抽象行时,它们通常会在错误的地方结束。
dsimcha 2011年

考虑更新您的问题,以澄清您所做的任何区别。整合评论序列是困难的。请更新问题以澄清和重点关注。
S.Lott

Answers:


116

这是一个定义问题。具有依赖项的测试是集成测试,而不是单元测试。您还应该具有集成测试套件。区别在于集成测试套件可以在不同的测试框架中运行,并且可能不作为构建的一部分,因为它们需要更长的时间。

对于我们的产品:每次构建都会运行我们的单元测试,只需几秒钟。我们的集成测试的一部分会在每次入住时运行,需要10分钟。我们的完整集成套件每天晚上运行,需要4个小时。


4
不要忘记使用回归测试来覆盖发现的和已修复的错误,以防止在将来的维护期间将其重新引入系统。
RBerteig

2
即使我同意这个定义,我也不会坚持。某些代码可能具有可接受的依赖项,例如StringFormatter,并且大多数单元测试仍将其考虑在内。
danidacar

2
danip:明确的定义很重要,我坚持使用。但是,认识到这些定义是目标也很重要。它们值得针对,但您并不总是需要靶心。单元测试通常会依赖于较低级别的库。
杰弗里·福斯特

2
了解外观测试的概念也很重要,它不测试组件如何组装在一起(例如集成测试),而是单独测试单个组件。(即对象图)
里卡多·罗德里格斯

1
它不仅要更快,而且要非常繁琐或难以管理,想象一下,如果不这样做,模拟的执行树可能会成倍增长。想象一下,如果您进一步推动模拟,可以在您的单元中模拟10个依赖关系,假设10个依赖关系中的1个在许多地方使用,并且它有20个自己的依赖关系,因此您必须模拟+ 20个其他依赖关系,可能在许多地方重复。在final- API时候你必须嘲笑-例如数据库,让你不必重新设置,当你告诉它的速度更快
FantomX1

39

放置所有依赖项的测试仍然很重要,但是正如Jeffrey Faust所说的,它更在集成测试领域。

单元测试最重要的方面之一就是使您的测试值得信赖。如果您不相信通过测试确实意味着一切都很好,而测试失败则意味着生产代码中存在问题,那么您的测试就没有那么有用了。

为了使您的测试值得信赖,您必须做一些事情,但是我将只关注其中一个答案。您必须确保它们易于运行,以便所有开发人员都可以在签入代码之前轻松地运行它们。“易于运行”意味着您的测试可以快速运行,并且不需要进行大量配置或设置即可使它们运行。理想情况下,任何人都应该能够签出代码的最新版本,立即运行测试,然后查看它们是否通过。

通过抽象化对其他事物(文件系统,数据库,Web服务等)的依赖关系,可以避免进行配置,并使您和其他开发人员更容易受到以下情况的诱惑:“哦,测试失败了,因为我没有这样做”还没有建立网络共享。哦,好,我稍后再运行。”

如果要测试对某些数据的处理方式,则针对该业务逻辑代码的单元测试不必关心如何获取这些数据。能够测试应用程序的核心逻辑而不必依赖于数据库等支持工具,真是太棒了。如果您不这样做,那么您会错失良机。

PS我应该补充一点,绝对有可能以可测试性为名进行过度设计。测试您的应用程序可以缓解这种情况。但是无论如何,糟糕的方法实现并不会降低方法的有效性。如果有人不停地问“我为什么要这样做?”,任何事情都可能被滥用和过度。在发展中。


就内部依赖而言,事情变得有些混乱。我想考虑的方式是,我想尽可能地保护我的班级,以免因错误的原因而改变。如果我有这样的设置...

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年

@dsimcha我扩展了我的答案,以进一步详细说明。希望能帮助到你。
亚当李尔

亚当,您能建议一种语言“如果SomeClass改变了,现在需要构造函数的参数了……我不必改变MyClass”是真的吗?抱歉,这是我的不寻常要求。我可以看到不必更改MyClass的单​​元测试,但不必更改MyClass ...哇。

1
@moz我的意思是,如果创建SomeClass的方式,则将MyClass注入其中而不是在内部创建,则无需更改MyClass。通常情况下,MyClass无需关心SomeClass的设置详细信息。如果SomeClass的界面发生了变化,那么是的...如果MyClass使用的任何方法受到影响,仍然必须对其进行修改。
亚当李尔

1
如果在测试MyClass时嘲笑SomeClass,如何检测MyClass是否使用了SomeClass错误或依赖于未经测试的SomeClass怪异(可能会改变)?
名叫'17

22

除了单元与集成测试问题外,请考虑以下因素。

类窗口小部件依赖于类Thingamajig和WhatsIt。

窗口小部件的单元测试失败。

问题在哪一类?

如果回答“启动调试器”或“通读代码直到找到它”,您将了解仅测试单元而不是依赖项的重要性。


3
@Brook:如何查看所有Widget依赖项的结果?如果它们全部通过,则除非有其他证明,否则Widget会出现问题。
dsimcha

4
@dsimcha,但现在您要在游戏中添加复杂性以检查中间步骤。为什么不简化而仅先进行简单的单元测试?然后进行集成测试。
asoundmove 2011年

1
@dsimcha,这听起来很合理,直到您进入一个非平凡的依赖关系图为止。假设您有一个复杂的对象,该对象具有3+层以上的依赖关系,它将变成O(N ^ 2)搜索问题,而不是O(1)
Brook

1
另外,我对SRP的解释是,一个类应该在概念/问题域级别上承担单一责任。当在较低的抽象级别上查看时,履行此职责是否要求它执行许多不同的操作并不重要。如果将SRP放到极限,则SRP将与OO相反,因为大多数类都保存数据并对其执行操作(以足够低的级别查看时有两件事)。
dsimcha 2011年

4
@Brook:更多类型本身并不会增加复杂性。如果这些类型在问题域中具有概念上的意义,并使代码更易于理解(而不是更难理解),则可以使用更多类型。问题是人为地将在概念上耦合到问题域级别的事物去耦合(即,您只有一个实现,可能永远不会有多个实现,等等),并且去耦合不能很好地映射到问题域概念。在这些情况下,围绕此实现创建严肃的抽象是愚蠢的,官僚的和冗长的。
dsimcha 2011年

14

想象一下编程就像烹饪。然后,单元测试就像确保您的食材新鲜,美味等一样。而集成测试就像确保您的饭菜美味一样。

最终,确保餐点美味(或系统正常工作)是最重要的事情,也是最终目标。但是,如果您的原料(单位)有效,那么您将可以找到更便宜的方法。

确实,如果您可以保证自己的单位/方法能够正常工作,那么您更有可能拥有一个运行良好的系统。我强调“更有可能”,而不是“某些”。您仍然需要进行集成测试,就像您仍然需要有人品尝您烹制的饭菜并告诉您最终产品很好一样。仅用新鲜的食材,您便会更轻松地到达那里。


7
并且当您的集成测试失败时,单元测试可能会解决该问题。
Tim Williscroft 2011年

9
打个比方:当您发现餐点味道很糟时,可能需要进行一些调查才能确定这是因为您的面包发霉了。但是,结果是您添加了一个单元测试,以在烹饪前检查发霉的面包,这样就不会再次发生该特定问题。然后,如果您以后再进餐失败,则可以消除发霉面包的原因。
Kyralessa

喜欢这个比喻!
咆哮者

6

通过完全隔离测试单元,可以测试该单元可以提交的所有数据变体和情况。由于它与其余部分隔离,因此您可以忽略对测试对象没有直接影响的变化。反过来,这将大大降低测试的复杂性。

集成了各种级别的依赖项的测试将使您能够测试单元测试中可能尚未测试的特定方案。

两者都很重要。仅进行单元测试,集成组件时就会不可避免地出现更复杂的细微错误。仅进行集成测试就意味着您在测试系统时没有信心对机器的各个部分进行测试。认为仅通过进行集成测试就可以实现更好的完整测试几乎是不可能的,因为添加的组件越多,条目组合的数量就变得非常快(请考虑阶乘),并且创建具有足够覆盖范围的测试变得非常快。

简而言之,我几乎在所有项目中通常使用三个级别的“单元测试”:

  • 单元测试与模拟或存根依赖性隔离,以测试单个组件的地狱。理想情况下,应尝试完全覆盖。
  • 集成测试可测试更细微的错误。精心设计的抽查会显示极限情况和典型情况。通常不可能完全覆盖,将精力集中在使系统发生故障的原因上。
  • 规格测试可将各种实际数据注入系统中,对数据进行检测(如果可能),并观察输出以确保其符合规格和业务规则。

依赖注入是实现此目标的一种非常有效的方法,因为它允许非常容易地隔离用于单元测试的组件,而不会增加系统的复杂性。您的业​​务场景可能不保证使用注入机制,但是您的测试场景几乎可以保证。对我而言,这足以使之成为必不可少的。您也可以使用它们通过部分集成测试来独立测试不同的抽象级别。


4

另一面是什么?单元测试不要同时测试其依赖关系真的很重要吗?如果是这样,为什么?

单元。表示单数。

测试2件东西意味着您拥有这两件东西以及所有功能依赖性。

如果添加第三项,则将线性增加测试中的功能依赖性。事物之间的互连比事物的数量增长得更快。

在被测试的n个项目中,存在n(n-1)/ 2个电位依存关系。

那是最大的原因。

简单有价值。


2

还记得您是如何第一次学习递归的吗?我的教授说:“假设您有一种方法可以计算x”(例如,解决任何x的fibbonacci问题)。“要解决x,您必须为x-1和x-2调用该方法”。同样,将依赖项存根也可以使它们假装存在并测试当前单元是否应执行的工作。当然,假设您正在严格测试依赖项。

本质上,这就是SRP在工作。即使对您的测试也只关注一个责任,就消除了您必须做的心理杂耍。


2

(这是次要的答案。感谢@TimWilliscroft的提示。)

在以下情况下,更易于定位故障:

  • 被测单元及其相关性均独立进行测试。
  • 当且仅当测试涵盖的代码中存在错误时,每个测试才会失败。

这在纸上效果很好。但是,如OP的说明所示(依赖关系是错误的),如果不对依赖关系进行测试,将很难查明故障的位置。


为什么不对依赖项进行测试?它们大概是较低的级别,并且更容易进行单元测试。此外,借助涵盖特定代码的单元测试,可以更轻松地确定故障的位置。
David Thornley

2

很多好的答案。我还要添加其他几点:

单元测试还允许您在不存在依赖项时测试代码。例如,您或您的团队尚未编写其他层,或者您正在等待其他公司提供的界面。

单元测试还意味着您不必在开发机器上拥有完整的环境(例如数据库,Web服务器等)。我强烈建议所有开发人员拥有这样的环境,但是为了减少错误等,请减少它。但是,如果由于某种原因无法模仿生产环境,则单元测试至少可以给您一定的水平在进入更大的测试系统之前,对代码充满信心。


0

关于设计方面:我认为,即使小型项目也可以从使代码可测试中受益。您不必引入诸如Guice之类的东西(通常会做一个简单的工厂类),但是将构造过程与编程逻辑分开会导致:

  • 通过其界面清楚地记录每个类的依赖关系(对团队中的新手很有帮助)
  • 这些类变得更加清晰和易于维护(一旦将丑陋的对象图创建放入单独的类中)
  • 松散耦合(使更改容易得多)

2
方法:是的,但是您必须添加额外的代码和额外的类才能执行此操作。代码应尽可能简洁,同时仍可读。恕我直言,添加工厂,而不是过度工程,除非您可以证明自己需要或很有可能需要它提供的灵活性。
dsimcha 2011年

0

嗯... 在这些答案中,单元和集成测试有很多要点!

我想念与成本相关的实用观点。也就是说,我清楚地看到了非常孤立的/原子的单元测试(可能彼此高度独立并且可以并行运行它们,并且没有任何依赖性,例如数据库,文件系统等)和(更高级别)集成测试的好处,但是...这也是成本(时间,金钱,...)和风险的问题

因此,在您从我的经验中想到“如何测试”之前,还有其他更重要的因素(例如“要测试什么”)...

我的客户是否(隐含地)为编写和维护测试的额外费用付费?在我的环境中(代码故障风险/成本分析,人员,设计规格,设置测试环境),一种更具测试驱动力的方法(在编写代码之前先编写测试)是否真的具有成本效益?代码始终是错误的,但是将测试转移到生产用途(在最坏的情况下!)会更具成本效益吗?

这也很大程度上取决于您的代码质量(标准)或框架,IDE,设计原则等。您和您的团队遵循的知识以及他们的经验如何。编写良好,易于理解,足够好的文档化(理想情况下是自我文档化)的模块化...引入的错误可能比反之少。因此,进行广泛测试的真正“需求”,压力或总体维护/错误修复成本/风险可能并不高。

让我们将其推到极致,在我的团队的一位同事建议的情况下,我们必须尝试对纯Java EE模型层代码进行单元测试,并为数据库中的所有类和模拟数据库提供所需的100%覆盖率。或希望集成测试覆盖所有可能的现实用例和Web ui工作流的100%的经理,因为我们不想冒任何用例失败的风险。但是我们有大约100万欧元的紧预算,有相当紧的计划来编写所有代码。在客户环境中,潜在的应用程序错误对人类或公司而言并不是很大的危险。我们的应用将通过(某些)重要的单元测试,集成测试,具有设计的测试计划的关键客户测试,测试阶段等进行内部测试。我们不为某些核工厂或制药企业开发应用!

我自己会尝试编写测试(如果可能),并在对代码进行单元测试时进行开发。但是我经常采用自顶向下的方法(集成测试)来进行操作,并试图找到要点,以便为重要的测试(通常在模型层中)进行“应用层削减”。(因为有关“图层”的信息很多)

此外,单元和集成测试代码不会对时间,金钱,维护,编码等产生负面影响。它很酷,应该被应用,但是在开始或进行许多开发测试的5年后,应谨慎考虑其影响。码。

因此,我要说的是,这实际上很大程度上取决于信任度和成本/风险/效益评估 ……就像在现实生活中,您不能也不想拥有大量或100%的安全机制。

这个孩子可以而且应该爬上某个地方,可能跌倒并伤到自己。由于我加注了错误的燃油(输入无效:),汽车可能会停止工作。如果时间按钮在3年后停止工作,可能会烧掉吐司。但是我从来没有想过要在高速公路上开车,将分离的方向盘握在手中:)

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.