伪造、模拟和存根之间有什么区别?

971
我知道如何使用这些术语,但我想知道单元测试中是否有关于"伪造"、"模拟"和"桩"的被广泛接受的定义?你是如何为你的测试定义这些术语的?描述一下你可能在哪些情况下使用它们。
以下是我对它们的理解:
"伪造":一个实现了接口但只包含固定数据和没有逻辑的类。根据实现返回"好"或"坏"的数据。
"模拟":一个实现了接口的类,允许动态设置特定方法返回的值/抛出的异常,并提供检查特定方法是否调用/未调用的能力。
"桩":类似于模拟类,但它不提供验证方法是否调用/未调用的能力。
生成方式如下:
- "模拟"和"桩"可以手动生成,也可以由模拟框架生成。 - "伪造类"需要手动生成。
使用方式如下:
  • 模拟对象主要用于验证我的类与依赖类之间的交互。
  • 存根一旦我验证了交互并测试了代码中的替代路径。
  • 虚拟类主要用于抽象出数据依赖,或者当模拟对象/存根设置每次都太繁琐时。

22
你在你的“问题”中已经说得很清楚了 :) 我认为这些都是这些术语相当被广泛接受的定义。 - Eran Galperin
6
维基百科关于Fake的定义与此不同,声称Fake“用作更简单的实现,例如在测试中使用内存数据库代替真正的数据库访问”。请参见 https://en.wikipedia.org/wiki/Test_double。 - zumalifeguard
7
我从以下资源中学到了很多,其中Robert C. Martin(叔叔鲍勃)的解释非常好:《The Little Mocker on The Clean Code Blog》。它解释了哑元、测试替身、存根、间谍、(真正的)模拟和伪物之间的区别和微妙之处。它还提到了Martin Fowler并简要解释了一些软件测试历史。 - Erik
1
请点击以下链接阅读有关测试替身的文章:https://testing.googleblog.com/2013/07/testing-on-toilet-know-your-test-doubles.html(一篇简短的一页摘要)。 - ShreevatsaR
1
这是我的解释:测试替身:伪对象、存根和模拟(带有示例的博客文章) - michal-lipski
“模拟和存根单元测试”没有任何“被接受的定义”,因为“单元”本身也没有定义。 - Konstantin Ivanov
16个回答

6

关键在于让测试表达清楚。如果我想让测试描述两个对象之间的关系,我会在Mock上设置期望。如果我要设置一个支持对象来帮助我在测试中获得有趣的行为,则使用存根返回值。


3
根据弗拉基米尔·科里科夫所著的《单元测试原理、实践与模式》一书:
  • 模拟对象:有助于模拟和检查外部交互。这些交互是系统在调用其依赖项以改变它们的状态时所做的调用。换句话说,它有助于检查SUT及其依赖项之间的交互(行为)。模拟对象可以是:
    1. 监视对象:手动创建。
    2. 模拟对象:使用框架创建。
  • 存根对象:有助于模拟输入交互。这些交互是SUT向其依赖项发出的调用,以获取输入数据。换句话说,它有助于测试传递给SUT的数据。它可以分为三种类型:
    1. 伪对象:通常用于替换尚不存在的依赖项。
    2. 虚拟对象:是硬编码值。
    3. 存根对象:完整依赖项,您可以对其进行配置,以针对不同的情况返回不同的值。

如果有人想知道SUT是什么,它的意思是“被测试系统”。 - Utkarsh Tiwari

3

存根仿真是对象,它们可以根据输入参数变化其响应。它们之间的主要区别在于,仿真比存根更接近实际实现。存根基本上包含对预期请求的硬编码响应。让我们看一个例子:

public class MyUnitTest {

 @Test
 public void testConcatenate() {
  StubDependency stubDependency = new StubDependency();
  int result = stubDependency.toNumber("one", "two");
  assertEquals("onetwo", result);
 }
}

public class StubDependency() {
 public int toNumber(string param) {
  if (param == “one”) {
   return 1;
  }
  if (param == “two”) {
   return 2;
  }
 }
}

Mock(模拟对象)比 Fake(假对象)和 Stub(桩对象)更高级。Mock提供与Stub相同的功能,但更为复杂。它们可以为自己定义规则,指定API上各个方法调用的顺序。大多数Mock可以跟踪方法被调用的次数,并根据该信息做出反应。Mock通常了解每个调用的上下文,并可以在不同情况下以不同方式做出反应。因此,Mock需要对其进行模拟的类有一定的了解。通常情况下,Stub无法跟踪方法被调用的次数或特定顺序。Mock的外观如下:

public class MockADependency {

 private int ShouldCallTwice;
 private boolean ShouldCallAtEnd;
 private boolean ShouldCallFirst;

 public int StringToInteger(String s) {
  if (s == "abc") {
   return 1;
  }
  if (s == "xyz") {
   return 2;
  }
  return 0;
 }

 public void ShouldCallFirst() {
  if ((ShouldCallTwice > 0) || ShouldCallAtEnd)
   throw new AssertionException("ShouldCallFirst not first thod called");
  ShouldCallFirst = true;
 }

 public int ShouldCallTwice(string s) {
  if (!ShouldCallFirst)
   throw new AssertionException("ShouldCallTwice called before ShouldCallFirst");
  if (ShouldCallAtEnd)
   throw new AssertionException("ShouldCallTwice called after ShouldCallAtEnd");
  if (ShouldCallTwice >= 2)
   throw new AssertionException("ShouldCallTwice called more than twice");
  ShouldCallTwice++;
  return StringToInteger(s);
 }

 public void ShouldCallAtEnd() {
  if (!ShouldCallFirst)
   throw new AssertionException("ShouldCallAtEnd called before ShouldCallFirst");
  if (ShouldCallTwice != 2) throw new AssertionException("ShouldCallTwice not called twice");
  ShouldCallAtEnd = true;
 }

}

2

来自Tomek Kaczanowski的JUnit和Mockito实用单元测试

在测试中,我们使用虚拟对象(Dummy)和存根对象(Stub)来为程序准备测试环境,它们并不用于验证。虚拟对象被用作参数值(例如直接方法调用的参数),而存根对象则向系统单元测试提供数据,代替其中的依赖对象。

测试桩件模拟对象的目的是验证系统单元与依赖对象之间的通信是否正确

假的几乎和真正的合作者一样好,但它在某种程度上更简单和/或更弱(这使得它不适合生产使用)。它通常也更“便宜”(即更快速或更简单设置),这使得它适用于测试(应尽可能快速运行)。一个典型的例子是,内存数据库可以代替完整的数据库服务器。它可以用于一些测试,因为它能很好地处理SQL请求;然而,在生产环境中,您不会想使用它。在测试中,假对象扮演着与虚拟对象和存根类似的角色:它是环境(测试装置)的一部分,而不是验证对象。假对象在集成测试中使用,而不是单元测试。


1
在 Gerard Meszaros 的 xUnit测试模式 一书中,有一张很好的表格,可以很好地说明差异。

enter image description here


0

我倾向于只使用两个术语 - FakeMock

Mock 只有在使用模拟框架(例如Moq)时才使用,因为当它是用new Mock<ISomething>()创建时,将其称为似乎不合适-尽管您可以技术上使用模拟框架来创建存根Fakes,但在这种情况下将其称为那样似乎很愚蠢 - 它必须是一个Mock

对于其他所有事情都使用Fake。 如果可以将概括为具有减少功能的实现,那么我认为Stub也可能是一个(如果不是,谁在乎,每个人都知道我的意思,从未有人说过"我认为那是一个Stub"


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接