模拟是否违反开放/封闭原则?


13

前段时间,我在找不到的Stack Overflow答案上读了一句话,该句子解释您应该测试公共API,而作者说您应该测试接口。作者还解释说,如果更改了方法实现,则无需修改测试用例,因为这样做会破坏确保被测系统正常工作的契约。换句话说,如果方法不起作用,则测试应该失败,但这不是因为实现发生了变化。

当我们谈论嘲笑时,这引起了我的注意。由于模拟在很大程度上依赖于被测系统依赖项的期望调用,因此模拟与实现紧密相关,而不是与接口紧密相关。

在研究模拟与存根时,有几篇文章认为应该使用存根而不是模拟,因为它们不依赖于依赖项的期望,这意味着测试不需要了解测试实施中的基础系统。

我的问题是:

  1. 模拟是否违反开放/封闭原则?
  2. 在上一段中支持存根的论点中是否缺少某些内容,使存根与模拟不那么好?
  3. 如果是这样,什么时候可以模拟一个好用例,什么时候可以使用存根?


8
Since mocking relays heavily on expectation calls from system under test's dependencies...我认为这是您要去的地方。模拟是外部系统的一些人为表示。 它不以任何方式表示外部系统,除非它以某种方式模拟外部系统,以便允许针对依赖于该外部系统的代码运行测试。您仍然需要进行集成测试,以证明您的代码可与真实的,未经模拟的系统一起使用。
罗伯特·哈维

8
换句话说,模拟是替代实现。 这就是为什么我们首先编写接口的原因,所以我们可以使用模拟作为实际实现的替代。换句话说,模拟是实际实现分离的,而不是与实际实现分离的。
罗伯特·哈维

3
“换句话说,如果方法不起作用,则测试应该失败,但这不是因为实现发生了变化”,这并不总是正确的。在很多情况下,您都应该更改实现和测试。
whatsisname 2015年

Answers:


4
  1. 我不明白为什么模拟会违反开放/封闭原则。如果您可以向我们解释为什么会这样,那么我们也许可以减轻您的担忧。

  2. 我能想到的存根的唯一缺点是,它们通常要比模拟编写更多的工作,因为它们每个实际上都是依赖接口的另一种实现,因此它通常必须提供完整的(或令人信服的完整)依赖接口的实现。举一个极端的例子,如果您的被测子系统调用RDBMS,则RDBMS的模拟将简单地响应被测子系统已知的特定查询,从而产生预定的测试数据集。另一方面,另一种实现可能是成熟的内存RDBMS,可能需要模拟生产中使用的实际客户端-服务器RDBMS的怪癖。(幸运的是,我们拥有HSQLDB之类的东西,因此我们实际上可以做到这一点,但是,

  3. 当依赖接口过于复杂而无法为其编写替代实现时,或者如果您确定只编写一次该模拟并且不再接触它,则可以很好地进行模拟。在这些情况下,请继续使用快速而肮脏的模拟。因此,存根(替代实现)的良好用例几乎是其他所有事情。尤其是如果您预计与被测子系统建立长期合作关系,那么绝对可以选择一种替代方案,该方案将非常简洁明了,并且仅在接口发生更改时才需要维护,而无需在接口时进行维护变化,并且每当被测子系统的实现发生变化时。

PS:您所指的人可能是我,在此是我在Programs.stackexchange.com上与测试有关的其他答案之一,例如,这个


an alternative implementation would be a full-blown in-memory RDBMS-您不必一定要存根那么远。
罗伯特·哈维

@RobertHarvey很好,对于HSQLDB和H2,要走那么远并不难。为了走那么远,做些半定的事情可能更困难。但是,如果您决定自己做,则必须先编写一个SQL解析器。当然,您可以偷工减料,但是还有很多工作要做。无论如何,正如我上面所说的,这只是一个极端的例子。
Mike Nakis 2015年

9
  1. 打开/关闭原则主要是关于能够在不修改类的情况下更改其行为。因此,在被测类中注入模拟的组件依赖关系不会违反它。

  2. 测试双打(模拟/存根)的问题是,您基本上对被测类如何与其环境交互进行了任意假设。如果这些期望是错误的,那么,一旦部署了代码,您可能会遇到一些问题。如果负担得起,请在与限制生产环境相同的约束下测试代码。如果不能,请尽可能少做假设,并仅对系统外围设备(数据库,身份验证服务,HTTP客户端等)进行模拟/存根。

恕我直言,应该使用double的唯一有效原因是,当您需要记录它与被测类的交互时,或者当您需要提供伪造数据时(两种技术都可以做到)。但是请小心,滥用它反映了不良的设计,或者测试过于依赖被测实现的API。


6

注意:我假设您将Mock定义为“没有实现的类,您可以监视的东西”,而Stub则是“部分模拟的,又使用了实现类的某些实际行为”,按照此Stack溢出问题

我不确定您为什么认为共识是使用存根,例如,在Mockito文档中恰好相反

像往常一样,您将阅读部分模拟警告:面向对象的编程通过将复杂度划分为单独的,特定的SRPy对象来解决复杂度问题。部分模拟如何适应这种范例?好吧,事实并非如此……部分模拟通常意味着复杂性已移至同一对象的不同方法。在大多数情况下,这不是您设计应用程序的方式。

但是,在少数情况下,局部模拟会派上用场:处理您无法轻松更改的代码(第三方接口,遗留代码的临时重构等)。但是,我不会将局部模拟用于新的,测试驱动的以及设计的代码。

该文档说的比我更好。使用模拟使您可以仅测试一个特定的类,而无需进行其他任何测试。如果您需要部分模拟来实现您想要的行为,则可能是做错了某些事情,违反了SRP等,并且您的代码可以进行重构。模仿不会违反开闭原则,因为无论如何它们只会在测试中使用,它们并不是对该代码的真正更改。通常,它们都是由cglib之类的库动态生成的。


2
从相同的提供的SO问题(可接受的答案)中,这也是我正在引用的Mock / Stub定义:Mock对象用于定义期望,即:在这种情况下,我希望使用此类参数调用方法A()。嘲讽记录并验证这种期望。另一方面,存根具有不同的目的:它们不记录或验证期望,而是允许我们“替换”“假”对象的行为,状态以利用测试场景……
Christopher Francisco

2

我认为问题可能出在以下假设上:唯一有效的测试是那些符合开放/封闭测试的测试。

显而易见,唯一重要的测试就是对接口进行测试。但是,实际上,通过测试内部工作情况来测试该接口通常更有效。

例如,几乎不可能测试任何否定要求,例如“该实现不得抛出任何异常”。考虑使用哈希表实现的地图接口。您希望确定哈希图满足map接口,并且不会抛出异常,即使它必须重新哈希事物(这可能会引起麻烦)。您可以测试输入的每种组合,以确保它们满足接口要求,但是所花费的时间可能比宇宙的热死时间还要长。取而代之的是,您稍微破坏一下封装,并开发出更紧密交互的模拟,从而迫使hashmap精确地执行所需的重新哈希处理,以确保重新哈希算法不会抛出。

Tl / Dr:“按部就班”做的很好,但是当一推再推时,到周五在老板桌上放一个产品要比按部就班的测试套件有用得多,后者要等到笔记本电脑的热死为止。宇宙确认一致性。

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.