首页 文章

伪造,嘲笑和抄袭有什么区别?

提问于
浏览
540

我知道我如何使用这些术语,但我想知道是否有单独测试的 fakingmockingstubbing 的接受定义?你如何为你的测试定义这些?描述您可能使用每种情况的情况 .

以下是我如何使用它们:

Fake :实现接口但包含固定数据且没有逻辑的类 . 只需返回"good"或"bad"数据,具体取决于实现 .

Mock :一个实现接口的类,允许动态设置值以返回/异常从特定方法抛出,并提供检查特定方法是否已被调用/未调用的能力 .

Stub :类似于模拟类,但它不提供验证方法是否已被调用/未调用的能力 .

模拟和存根可以由模拟框架手动生成或生成 . 伪造的类是手工生成的 . 我主要使用模拟来验证我的类和依赖类之间的交互 . 一旦我验证了交互并且正在通过我的代码测试备用路径,我就使用存根 . 我主要使用伪类来抽象出数据依赖性,或者每次使用模拟/存根都太繁琐 .

8 回答

  • 75

    你可以得到一些信息:

    来自Martin Fowler关于Mock和Stub

    Fake 对象实际上有工作实现,但通常需要一些使它们不适合 生产环境 的快捷方式

    Stubs 为测试期间发出的呼叫提供固定答案,通常不会对's programmed in for the test. Stubs may also record information about calls, such as an email gateway stub that remembers the messages it '发送的所有内容做出回应', or maybe only how many messages it '发送' .

    Mocks 就是我们在这里讨论的内容:预期编程的对象具有预期,这些对象构成了预期接收的呼叫的规范 .

    来自xunitpattern:

    Fake :我们获取或构建一个非常轻量级的实现,其功能与SUT所依赖的组件所提供的功能相同,并指示SUT使用它而不是真实的 .

    Stub :此实现配置为响应来自SUT的调用,其中包含将执行SUT内未测试代码(请参见第X页上的 生产环境 错误)的值(或例外) . 使用测试存根的关键指示是由于无法控制SUT的间接输入而导致的未测试代码

    Mock Object 实现与SUT(被测系统)所依赖的对象相同的接口 . 当我们需要进行行为验证时,我们可以使用Mock对象作为观察点,以避免因无法观察SUT上调用方法的副作用而导致出现未经测试的要求(请参阅第X页的 生产环境 错误) .

    个人而言

    我试着通过使用:Mock和Stub进行简化 . 当它是一个返回一个设置为测试类的值的对象时,我使用Mock . 我使用Stub来模拟要测试的Interface或Abstract类 . 事实上,你所谓的它并不重要,它们都是未在 生产环境 中使用的类,并且被用作测试的实用程序类 .

  • 2

    Stub - 提供方法调用的预定义答案的对象 .

    Mock - 您设定期望的对象 .

    Fake - 具有有限能力的对象(用于测试目的),例如虚假的网络服务 .

    Test Double是短柱,模拟和假货的总称 . 但非正式地说,你经常听到人们只是称他们为嘲笑 .

  • 436

    我很惊讶这个问题已经存在了很长时间,而且还没有人提供基于Roy Osherove's "The Art of Unit Testing"的答案 .

    在“3.1 Introducing stubs”中,将stub定义为:

    存根是系统中现有依赖关系(或协作者)的可控替代 . 通过使用存根,您可以在不直接处理依赖关系的情况下测试代码 .

    并定义存根和模拟之间的区别:

    关于模拟与存根的主要关注点是模拟就像存根一样,但你对模拟对象断言,而你没有断言存根 .

    假只是用于存根和模拟的名称 . 例如,当您不关心存根和模拟之间的区别时 .

    Osherove区分存根和模拟的方式意味着任何用作测试假的类都可以是存根或模拟 . 对于特定测试而言,完全取决于您在测试中编写检查的方式 .

    • 当你的测试检查被测试的类中的值时,或实际上除了假的以外的任何地方,假的被用作存根 . 它只是为要使用的类提供了值,可以直接通过调用返回的值,也可以通过调用它来间接导致副作用(在某些状态下) .

    • 当您的测试检查伪造的值时,它被用作模拟 .

    将FakeX类用作存根的测试示例:

    const pleaseReturn5 = 5;
    var fake = new FakeX(pleaseReturn5);
    var cut = new ClassUnderTest(fake);
    
    cut.SquareIt;
    
    Assert.AreEqual(25, cut.SomeProperty);
    

    fake 实例用作存根,因为 Assert 根本不使用 fake .

    使用测试类X的测试示例模拟:

    const pleaseReturn5 = 5;
    var fake = new FakeX(pleaseReturn5);
    var cut = new ClassUnderTest(fake);
    
    cut.SquareIt;
    
    Assert.AreEqual(25, fake.SomeProperty);
    

    在这种情况下, Assert 检查 fake 上的值,使该伪造成模拟 .

    现在,这些例子当然是非常人为的,但我认为这种区别很有用 . 它让您了解如何测试您的东西以及测试的依赖性 .

    我同意Osherove的观点

    从纯粹的可维护性角度来看,在我的测试中使用模拟会比不使用它们产生更多麻烦 . 这是我的经历,但我总是在学习新的东西 .

    断言假冒是你真正想要避免的,因为它会使你的测试高度依赖于一个完全不受测试的类的实现 . 这意味着类 ActualClassUnderTest 的测试可以开始中断,因为 ClassUsedAsMock 的实现已更改 . 这给我带来了难闻的气味 . ActualClassUnderTest 的测试最好只在 ActualClassUnderTest 更改时中断 .

    我意识到写假冒伪劣是一种常见的做法,特别是当你是一个模仿者类型的TDD用户时 . 我想我与古典主义阵营中的Martin Fowler坚定不移(见Martin Fowler's "Mocks aren't Stubs"),并且像Osherove一样尽可能避免交互测试(这只能通过断言对付假冒) .

    为了有趣的阅读为什么你应该避免这里定义的模拟,谷歌为“福勒嘲笑古典主义者” . 你会发现很多意见 .

  • 0

    为了说明存根和模拟的用法,我还想提供一个基于Roy Osherove的“The Art of Unit Testing”的例子 .

    想象一下,我们有一个LogAnalyzer应用程序,它具有打印日志的唯一功能 . 它不仅需要与Web服务通信,而且如果Web服务引发错误,LogAnalyzer必须将错误记录到不同的外部依赖项,通过电子邮件将其发送给Web服务管理员 .

    这是我们想在LogAnalyzer中测试的逻辑:

    if(fileName.Length<8)
    {
     try
      {
        service.LogError("Filename too short:" + fileName);
      }
     catch (Exception e)
      {
        email.SendEmail("a","subject",e.Message);
      }
    }
    

    当Web服务抛出异常时,如何测试LogAnalyzer是否正确调用电子邮件服务?以下是我们面临的问题:

    • 我们如何更换网络服务?

    • 我们如何模拟Web服务中的异常,以便我们可以测试对电子邮件服务的调用?

    • 我们如何才能知道电子邮件服务是否正确调用?

    我们可以通过 using a stub for the web service 处理前两个问题 . 要解决第三个问题,我们可以 use a mock object for the email service .

    假是一个通用术语,可用于描述存根或模拟 . 在我们的测试中,我们将有两个假货 . 一个是电子邮件服务模拟,我们将用它来验证是否已将正确的参数发送到电子邮件服务 . 另一个将是一个存根,我们将用它来模拟从Web服务抛出的异常 . 这是一个存根,因为我们不会使用Web服务假来验证测试结果,只是为了确保测试正确运行 . 电子邮件服务是一个模拟,因为我们会断言它被正确调用 .

    [TestFixture]
    public class LogAnalyzer2Tests
    {
    [Test]
     public void Analyze_WebServiceThrows_SendsEmail()
     {
       StubService stubService = new StubService();
       stubService.ToThrow= new Exception("fake exception");
       MockEmailService mockEmail = new MockEmailService();
    
       LogAnalyzer2 log = new LogAnalyzer2();
       log.Service = stubService
       log.Email=mockEmail;
       string tooShortFileName="abc.ext";
       log.Analyze(tooShortFileName);
    
       Assert.AreEqual("a",mockEmail.To); //MOCKING USED
       Assert.AreEqual("fake exception",mockEmail.Body); //MOCKING USED
       Assert.AreEqual("subject",mockEmail.Subject);
    
     }
    }
    
  • 167

    这是使测试具有表现力的问题 . 如果我想让测试描述两个对象之间的关系,我会对模拟设置期望 . 如果我正在设置支持对象以使我了解测试中的有趣行为,那么我会返回值 .

  • 5

    如果您熟悉Arrange-Act-Assert,那么解释存根和模拟之间可能对您有用的区别的一种方法是存根属于排列部分,因为它们用于排列输入状态,并且模拟属于断言部分,因为它们用于断言结果 .

    假人什么都不做 . 它们只是用于填充参数列表,因此您不会得到未定义或null错误 . 它们也存在以满足严格类型语言的类型检查器,因此您可以被允许编译和运行 .

  • 7

    你断言它的东西被称为 mock 对象,其他一切只是帮助测试运行,是 stub .

  • 1

    stubfake 是对象,因为它们可以根据输入参数改变响应 . 它们之间的主要区别在于假冒更接近现实世界的实施而不是存根 . 存根基本上包含对预期请求的硬编码响应 . 让我们看一个例子:

    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 是假货和存根的一个升级 . 模拟提供与存根相同的功能,但更复杂 . 他们可以为他们定义规则,规定必须调用其API上的方法的顺序 . 大多数模拟可以跟踪调用方法的次数,并可以根据该信息做出反应 . 模拟通常知道每个调用的上下文,并且可以在不同情况下做出不同的反应 . 因此,模拟需要一些他们正在嘲笑的类的知识 . 存根通常无法跟踪调用方法的次数或调用方法序列的顺序 . 模拟看起来像:

    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;
     }
    
    }
    

相关问题