是否有对所有公开进行存根和模拟的单元测试的意义?


58

当以“适当”的方式进行单元测试时,即对每个公用电话进行存根并返回预设值或模拟,我觉得我实际上并没有进行任何测试。我从字面上看我的代码,并基于通过公共方法的逻辑流创建示例。每次实现更改时,我都必须再次更改那些测试,而不是真的感觉自己正在完成任何有用的事情(无论是中期还是长期的)。我也进行集成测试(包括不愉快的路径),我并不介意增加测试时间。有了这些,我觉得我实际上是在测试回归,因为它们已经捕获了多个,而单元测试所做的只是告诉我,我已经知道,公共方法的实现已更改。

单元测试是一个广泛的话题,我觉得我是一个不了解这里内容的人。与集成测试(不包括时间开销)相比,单元测试的决定性优势是什么?


4
我的两分钱:不要过度使用模拟游戏
Juan Mendes

1
另请参阅
模拟

Answers:


37

当以“适当”的方式进行单元测试时,即对每个公用电话进行存根并返回预设值或模拟,我觉得我实际上并没有进行任何测试。我从字面上看我的代码,并基于通过公共方法的逻辑流创建示例。

这听起来像您正在测试的方法需要其他几个类实例(您必须对其进行模拟),并自行调用多个方法。

出于您概述的原因,此类代码确实很难进行单元测试。

我发现有帮助的是将这些类分为:

  1. 具有实际“业务逻辑”的类。它们很少或不使用对其他类的调用,并且易于测试(值输入-值输出)。
  2. 与外部系统(文件,数据库等)交互的类。这些包装了外部系统,并为您提供了方便的界面。
  3. 将“所有内容捆绑在一起”的类

然后,来自1.的类很容易进行单元测试,因为它们只接受值并返回结果。在更复杂的情况下,这些类可能需要自己执行调用,但是它们只会从2开始调用类(而不是直接调用例如数据库函数),并且从2开始易于模仿(因为它们仅展示所需包装系统的各个部分)。

通常无法对2和3.中的类进行有意义的单元测试(因为它们自己没有做任何有用的事情,它们只是“胶水”代码)。OTOH,这些类往往相对简单(很少),因此集成测试应充分涵盖它们。


一个例子

一堂课

假设您有一个类,该类从数据库检索价格,应用一些折扣,然后更新数据库。

如果将所有这些都放在一个类中,则需要调用很难模拟的DB函数。用伪代码:

1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database

所有这三个步骤都需要数据库访问,因此需要进行很多(复杂的)模拟,如果代码或数据库结构发生更改,则很可能会破坏模拟。

分开

您分为三类:PriceCalculation,PriceRepository,App。

PriceCalculation仅进行实际计算,并提供所需的值。应用将所有内容捆绑在一起:

App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices

那样:

  • PriceCalculation封装了“业务逻辑”。测试很容易,因为它不会自己调用任何东西。
  • 通过建立模拟数据库并测试读取和更新调用,可以对PriceRepository进行伪单元测试。它几乎没有逻辑,因此代码路径很少,因此您不需要太多的测试。
  • 无法对应用进行有意义的单元测试,因为它是粘合代码。但是,它也非常简单,因此集成测试就足够了。如果以后的应用程序变得太复杂,则需要扩展更多的“业务逻辑”类。

最后,事实证明PriceCalculation必须执行自己的数据库调用。例如,因为只有PriceCalculation知道其需要哪些数据,所以App无法提前获取它。然后,您可以将其传递给PriceRepository(或其他一些存储库类)的实例,并根据PriceCalculation的需求进行定制。然后需要模拟该类,但这将很简单,因为PriceRepository的接口很简单,例如PriceRepository.getPrice(articleNo, contractType)。最重要的是,PriceRepository的接口将PriceCalculation与数据库隔离,因此对数据库模式或数据组织的更改不太可能更改其接口,从而破坏了模拟。


5
我以为我自己一个人没看到单元测试中所有内容的意义,谢谢
投入

4
当您说类型3的类很少时,我只是不同意,我觉得我的大多数代码都是类型3,几乎没有业务逻辑。这就是我的意思:stackoverflow.com/questions/38496185/…–
罗德里戈·鲁伊斯

27

与集成测试相比,单元测试的决定性优势是什么?

这是错误的二分法。

单元测试和集成测试有两个相似但不同的目的。单元测试的目的是确保您的方法有效。实际上,单元测试确保代码符合单元测试概述的约定。 这在单元测试的设计方式中很明显:它们明确说明了代码应该执行的操作,并断言代码可以执行此操作。

集成测试是不同的。集成测试可进行软件组件之间的交互。您可以使软件组件通过所有测试,但仍无法通过集成测试,因为它们无法正确交互。

但是,如果单元测试具有决定性的优势,那就是:单元测试比集成测试更容易设置,并且所需的时间和精力要少得多。 如果使用得当,单元测试会鼓励开发“可测试”代码,这意味着最终结果将变得更加可靠,更易于理解和维护。可测试的代码具有某些特征,例如一致的API,可重复的行为,并且返回易于断言的结果。

集成测试更困难,也更昂贵,因为您经常需要精心的模拟,复杂的设置和困难的断言。在系统集成的最高级别,想象一下尝试在UI中模拟人机交互。整个软件系统都致力于这种自动化。我们追求的是自动化。人工测试是不可重复的,并且不会像自动化测试那样扩展。

最后,集成测试不能保证代码覆盖率。您正在使用集成测试测试多少个代码循环,条件和分支组合?你真的知道吗 您可以将一些工具与单元测试和被测方法一起使用,这些工​​具将告诉您您有多少代码覆盖率以及代码的循环复杂性。但是它们仅在单元测试所在的方法级别上才能真正发挥作用。


如果每次重构时测试都在变化,那就是另一个问题。单元测试应该是关于记录软件的功能,证明它可以做到的,然后在重构底层实现时再次证明它可以做到的。如果您的API发生了更改,或者您需要根据系统设计的更改来更改您的方法,那就应该发生这种情况。如果发生的事情很多,请考虑在编写代码之前先编写测试。 这将迫使您考虑整体架构,并允许您使用已建立的API编写代码。

如果您花费大量时间为诸如这样的简单代码编写单元测试

public string SomeProperty { get; set; }

那么您应该重新检查您的方法。单元测试应该用来测试行为,并且上面的代码行中没有行为。但是,您已经在代码中的某个地方创建了一个依赖项,因为几乎可以肯定该属性将在代码的其他地方被引用。与其这样做,不如考虑编写将所需属性作为参数的方法:

public string SomeMethod(string someProperty);

现在,您的方法不再依赖于自身之外的任何内容,并且由于完全独立,因此现在更具可测试性。当然,您将无法始终做到这一点,但是它确实使代码朝着更具可测试性的方向发展,而这一次您是针对实际行为编写单元测试


2
我知道单元测试和集成测试有不同的目标,但是,如果您对单元测试进行的所有公共调用进行存根和模拟,我仍然不知道单元测试如何有用。如果不是存根和模拟的话,我会理解“代码符合单元测试概述的约定”。我的单元测试实际上是我正在测试的方法内部逻辑的反映。您(I)并没有真正测试任何东西,只是查看您的代码,然后将其“转换”为测试。关于自动化的难度和代码覆盖率,我目前正在使用Rails,并且它们都得到了很好的照顾。
13年

2
如果您的测试只是方法逻辑的反映,那么您做错了。单元测试本质上应该为该方法提供一个值,接受一个返回值,并断言该返回值应该是什么。不需要逻辑就可以做到这一点。
罗伯特·哈维

2
很有道理,但仍然必须对所有其他公共调用(db,某些“全局变量”,例如当前用户状态等)进行存根处理,并按照方法逻辑进行测试代码。
13年

1
所以我猜单元测试是针对大多数孤立的东西的,这些东西可以确认“输入集->预期结果集”?
13年

1
我在创建大量单元和集成测试中的经验(更不用说那些测试使用的高级模拟,集成测试和代码覆盖工具)与您在这里的大多数主张相矛盾:1)“单元测试的目的是确保您的代码完成了预期的任务”:集成测试也是如此(甚至更多);2)“单元测试更容易设置”:不,不是(很多时候,集成测试更容易);3)“如果使用得当,单元测试会鼓励开发“可测试”代码”:与集成测试相同;(续)
罗杰里奥2015年

4

模拟的单元测试是为了确保类的实现是正确的。您模拟正在测试的代码的依赖项的公共接口。这样,您可以控制类外部的所有内容,并确保测试失败是由于类内部的某种原因而不是其他对象之一引起的。

您还将测试被测类的行为而不是实现。如果您重构代码(创建新的内部方法等),则单元测试应该不会失败。但是,如果您要更改公共方法的操作,则绝对会导致测试失败,因为您已更改了行为。

听起来也像是在编写代码之后就在编写测试,请尝试先编写测试。尝试概述类应具有的行为,然后编写最少的代码以使测试通过。

单元测试和集成测试都对确保代码质量很有用。单元测试隔离地检查每个组件。集成测试可确保所有组件正确交互。我想在测试套件中同时使用这两种类型。

单元测试可以帮助我一次开发,因为我可以一次专注于一个应用程序。模拟我还没有制作的组件。它们也非常适合回归,因为它们记录了我发现的逻辑中的任何错误(即使在单元测试中)。

更新

创建一个仅确保方法被调用的测试具有价值,因为您可以确保方法确实被调用。特别是如果您首先编写测试,则会有一份需要进行的方法检查清单。由于此代码几乎是过程性的,因此除了调用方法外,您无需检查太多。您正在保护代码以备将来更改。当您需要先调用另一个方法时。否则即使初始方法引发异常,方法也总是会被调用。

此方法的测试可能永远不会更改,或者只有在更改方法时才可能更改。为什么这是一件坏事?它有助于加强使用测试。如果必须在更改代码后修复测试,则将养成使用代码更改测试的习惯。


如果一个方法没有调用任何私有的方法,那么对它进行单元测试就没有意义了,对吗?
13年

如果该方法是私有方法,则无需显式测试它,而应通过公共接口对其进行测试。应该测试所有公共方法,以确保行为正确。
Schleis

不,我的意思是,如果公用方法未调用任何私有方法,测试该公用方法是否有意义?
13年

是。该方法不是吗?因此,应该对其进行测试。从测试的角度来看,我不知道它是否使用了私有的东西。我只知道,如果我提供输入A,我应该得到的输出B.
Schleis

哦,是的,该方法执行某些操作,而某些操作则调用了其他公共方法(仅此而已)。因此,您将“适当地”测试对调用返回值进行存根的方法,然后设置消息期望值。在这种情况下,您到底要测试什么?拨打正确的电话吗?好吧,您编写了该方法,您可以对其进行查看并确切地了解它的作用。我认为单元测试更适合用于应该像“输入->输出”之类的隔离方法,因此您可以设置一些示例,然后在重构时进行回归测试。
13年

3

我遇到了类似的问题-直到发现组件测试的强大功能。简而言之,它们与单元测试相同,除了默认情况下您不模拟而是使用真实对象(理想情况下是通过依赖注入)。

这样,您可以快速创建具有良好代码覆盖率的强大测试。无需一直更新您的模拟。这可能比使用100%模拟的单元测试的精度低一些,但是节省的时间和金钱可以弥补这一点。您真正需要使用模拟或固定装置的唯一内容是存储后端或外部服务。

实际上,过度嘲弄是一种反模式:TDD反模式Mo俩是邪恶的


0

尽管操作已经标记了答案,但我在这里仅加2美分。

与集成测试(不包括时间开销)相比,单元测试的决定性优势是什么?

并回应

当以“适当”的方式进行单元测试时,即对每个公用电话进行存根并返回预设值或模拟,我觉得我实际上并没有进行任何测试。

OP提出了一个有用但并非完全正确的要求:

单元测试有效,但是仍然存在错误?

从我在测试套件方面的很少经验,我了解到单元测试始终用于测试类的最基本方法级别的功能。我认为,公共,私有或内部的每种方法都应具有专门的单元测试。即使以我最近的经验,我也有一个公共方法,正在调用其他小型私有方法。因此,有两种方法:

  1. 不要为私有方法创建单元测试。
  2. 为私有方法创建一个单元测试。

如果您从逻辑上考虑,那么拥有私有方法的重点是:主要的公共方法变得太大或混乱。为了解决此问题,您需要进行明智的重构,并创建一小段代码,这些代码应作为单独的私有方法使用,从而使您的主要公共方法的体积减少。请记住,此私有方法稍后可能会被重用。在某些情况下,没有其他公共方法取决于该私有方法,而是谁知道未来。


考虑私有方法被许多其他公共方法重用的情况。

因此,如果我选择方法1:我将重复单元测试,并且它们将很复杂,因为您在公共方法和私有方法的每个分支中都有大量的单元测试。

如果选择方法2:为单元测试编写的代码相对较少,并且测试起来会容易得多。


考虑不重用私有方法的情况 没有必要为该方法编写单独的单元测试。

至于集成测试,它们往往是详尽无遗的,并且更高级。他们会告诉您,输入后,您所有的类都应得出最终结论。要了解有关集成测试有用性的更多信息,请参见所提到的链接。

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.