【问题标题】:What is the difference between mocking and spying when using Mockito?使用 Mockito 时,模拟和间谍有什么区别?
【发布时间】:2013-02-09 18:30:26
【问题描述】:

使用 Mockito 间谍的用例是什么?

在我看来,每个间谍用例都可以通过模拟处理,使用 callRealMethod。

我可以看到的一个区别是,如果您希望大多数方法调用是真实的,它可以节省一些代码行来使用模拟与间谍。是这样还是我错过了更大的图景?

【问题讨论】:

    标签: java testing mocking mockito


    【解决方案1】:

    [Test double types]

    MockSpy

    Mock 是一个裸双对象。此对象具有相同的方法签名,但实现为空并返回默认值 - 0 和 null

    Spy 是一个克隆的双重对象。新对象是基于 真实 对象克隆的,但您可以模拟它

    class A {
        String foo1() {
            foo2();
            return "RealString_1";
        }
    
        String foo2() {
            return "RealString_2";
        }
    
        void foo3() { foo4(); }
    
        void foo4() { }
    }
    
    @Test
    public void testMockA() {
        //given
        A mockA = Mockito.mock(A.class);
        Mockito.when(mockA.foo1()).thenReturn("MockedString");
    
        //when
        String result1 = mockA.foo1();
        String result2 = mockA.foo2();
    
        //then
        assertEquals("MockedString", result1);
        assertEquals(null, result2);
    
        //Case 2
        //when
        mockA.foo3();
    
        //then
        verify(mockA).foo3();
        verify(mockA, never()).foo4();
    }
    
    @Test
    public void testSpyA() {
        //given
        A spyA = Mockito.spy(new A());
    
        Mockito.when(spyA.foo1()).thenReturn("MockedString");
    
        //when
        String result1 = spyA.foo1();
        String result2 = spyA.foo2();
    
        //then
        assertEquals("MockedString", result1);
        assertEquals("RealString_2", result2);
    
        //Case 2
        //when
        spyA.foo3();
    
        //then
        verify(spyA).foo3();
        verify(spyA).foo4();
    }
    

    【讨论】:

      【解决方案2】:

      答案在the documentation

      真正的部分模拟(自 1.8.0 起)

      最后,在邮件列表上进行了多次内部辩论和讨论后,部分模拟支持被添加到 Mockito。以前我们将部分模拟视为代码异味。但是,我们发现了部分模拟的合法用例。

      在 1.8 版之前,spy() 无法生成真正的部分模拟,这让一些用户感到困惑。阅读有关间谍的更多信息:here 或在 javadoc 中了解 spy(Object) 方法。

      callRealMethod() 是在spy() 之后引入的,但 spy() 当然是留在那里,以确保向后兼容。

      否则,你是对的:间谍的所有方法都是真实的,除非被存根。除非调用callRealMethod(),否则模拟的所有方法都会被存根。一般来说,我更喜欢使用callRealMethod(),因为它不会强迫我使用doXxx().when() 成语而不是传统的when().thenXxx()

      【讨论】:

      • 在这些情况下,优先使用 mock 而不是 spy 的问题是,当类使用未注入其中的成员(但在本地初始化),并且稍后由“真实”方法使用时;在 mock 中,成员将被初始化为其默认 Java 值,这可能会导致错误行为甚至 NullPointerException。通过它的方法是添加一个“init”方法,然后“真正”调用它,但这对我来说似乎有点过头了。
      • 来自文档:“间谍应谨慎使用,偶尔使用,例如在处理遗留代码时。”单元测试领域有太多做同样事情的方法。
      【解决方案3】:

      当您想为遗留代码创建单元测试时,Spy 会很有用。

      我在这里创建了一个可运行的示例 https://www.surasint.com/mockito-with-spy/ ,我在这里复制了一些。

      如果你有类似这样的代码:

      public void transfer(  DepositMoneyService depositMoneyService, WithdrawMoneyService withdrawMoneyService, 
                   double amount, String fromAccount, String toAccount){
          withdrawMoneyService.withdraw(fromAccount,amount);
          depositMoneyService.deposit(toAccount,amount);
      }
      

      您可能不需要间谍,因为您可以模拟 DepositMoneyService 和 WithdrawMoneyService。

      但是对于一些遗留代码,依赖关系在这样的代码中:

          public void transfer(String fromAccount, String toAccount, double amount){
      
              this.depositeMoneyService = new DepositMoneyService();
              this.withdrawMoneyService = new WithdrawMoneyService();
      
              withdrawMoneyService.withdraw(fromAccount,amount);
              depositeMoneyService.deposit(toAccount,amount);
          }
      

      是的,您可以更改为第一个代码,然后更改 API。如果很多地方都在使用这种方法,则必须全部更改。

      另一种方法是您可以像这样提取依赖项:

          public void transfer(String fromAccount, String toAccount, double amount){
              this.depositeMoneyService = proxyDepositMoneyServiceCreator();
              this.withdrawMoneyService = proxyWithdrawMoneyServiceCreator();
      
              withdrawMoneyService.withdraw(fromAccount,amount);
              depositeMoneyService.deposit(toAccount,amount);
          }
          DepositMoneyService proxyDepositMoneyServiceCreator() {
              return new DepositMoneyService();
          }
      
          WithdrawMoneyService proxyWithdrawMoneyServiceCreator() {
              return new WithdrawMoneyService();
          }
      

      然后你可以像这样使用 spy 注入依赖:

      DepositMoneyService mockDepositMoneyService = mock(DepositMoneyService.class);
              WithdrawMoneyService mockWithdrawMoneyService = mock(WithdrawMoneyService.class);
      
          TransferMoneyService target = spy(new TransferMoneyService());
      
          doReturn(mockDepositMoneyService)
                  .when(target).proxyDepositMoneyServiceCreator();
      
          doReturn(mockWithdrawMoneyService)
                  .when(target).proxyWithdrawMoneyServiceCreator();
      

      以上链接中的更多详细信息。

      【讨论】:

        【解决方案4】:

        间谍和模拟之间的区别

        当 Mockito 创建一个模拟时——它是从一个类型的类中创建的,而不是从实际实例中创建的。 mock 只是创建了 Class 的一个基本的 shell 实例,完全用于跟踪与它的交互。另一方面,间谍将包装现有实例。它仍然会以与普通实例相同的方式运行——唯一的区别是它还将被检测为跟踪与它的所有交互。

        在下面的例子中——我们创建了一个 ArrayList 类的模拟:

        @Test
        public void whenCreateMock_thenCreated() {
            List mockedList = Mockito.mock(ArrayList.class);
        
            mockedList.add("one");
            Mockito.verify(mockedList).add("one");
        
            assertEquals(0, mockedList.size());
        }
        

        如你所见——在模拟列表中添加一个元素实际上并没有添加任何东西——它只是调用方法而没有其他副作用。另一方面,间谍的行为会有所不同——它实际上会调用 add 方法的真正实现并将元素添加到底层列表中:

        @Test
        public void whenCreateSpy_thenCreate() {
            List spyList = Mockito.spy(new ArrayList());
            spyList.add("one");
            Mockito.verify(spyList).add("one");
        
            assertEquals(1, spyList.size());
        }
        

        这里我们可以肯定地说,对象真正的内部方法被调用了,因为当你调用 size() 方法时,你得到的 size 为 1,但这个 size() 方法并没有被模拟! 那么 1 来自哪里? 内部实际 size() 方法被称为 size() 没有被模拟(或存根),因此我们可以说条目是添加到真实对象中的。

        来源:http://www.baeldung.com/mockito-spy + 自记。

        【讨论】:

        • 你不是说 size() 返回 1 吗?
        • 在第一个例子中,为什么 mockedList.size() 返回 0 如果该方法也没有被删除?给定方法的返回类型,这只是一个默认值吗?
        • @mike: mockedList.size() 返回一个int 并且int 在Java 中的默认值是0。如果您尝试在mockedList.clear(); 之后执行assertEquals(0, mockedList.size());,结果保持不变。
        • 这个答案写得很好,很简单,帮助我最终理解了模拟和间谍之间的区别。不错。
        【解决方案5】:

        如果有一个包含 8 个方法的对象,并且您有一个测试要调用 7 个真实方法并存根一个方法,那么您有两个选择:

        1. 使用模拟,您必须通过调用 7 来设置它 callRealMethod 和存根方法
        2. 使用spy,您必须通过存根一个方法来设置它

        doCallRealMethod 上的 official documentation 建议使用间谍进行部分模拟。

        另请参阅 javadoc spy(Object) 以了解有关部分模拟的更多信息。 Mockito.spy() 是创建部分模拟的推荐方法。这 原因是它保证正确调用真正的方法 构造对象,因为您负责构造 对象传递给 spy() 方法。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-10-09
          • 1970-01-01
          • 2017-07-06
          • 2017-07-12
          • 1970-01-01
          • 2011-10-09
          相关资源
          最近更新 更多