Mockito - @Spy与@Mock的区别

185
我了解到,Spy会在对象上调用真实的方法,而Mock会在模拟对象上调用方法。此外,如果没有代码问题,应避免使用Spy。
然而,我该如何使用Spy?它们与Mock有什么不同呢?

2
可能是mockito mock vs. spy的重复问题。 - rds
9个回答

158

从技术上讲,“mocks”和“spies”都是一种特殊类型的“测试替身(test doubles)”。

但不幸的是,Mockito对此进行了奇怪的区分。

在其他模拟框架中,mockito中的"mock"是普通的mock(允许您存根调用;也就是说,从方法调用中返回特定的值)。

在其他模拟框架中,mockito中的"spy"是部分mock(对象的一部分将被mock,而另一部分将使用实际的方法调用)。


126

两者都可用于模拟方法或字段,不同之处在于mock中你需要创建一个完整的模拟或虚假对象,而在spy中是使用真实的对象并且只是监视或存根特定的方法。

当使用spy对象时,因为它是一个真实的方法,当你没有存根该方法时,它将调用真实的方法行为。如果你想改变和模拟该方法,则需要存根该方法。

考虑下面的示例作为比较。

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
 
import java.util.ArrayList;
import java.util.List;
 
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
 
@RunWith(MockitoJUnitRunner.class)
public class MockSpy {
 
    @Mock
    private List<String> mockList;
 
    @Spy
    private List<String> spyList = new ArrayList();
 
    @Test
    public void testMockList() {
        //by default, calling the methods of mock object will do nothing
        mockList.add("test");

        Mockito.verify(mockList).add("test");
        assertEquals(0, mockList.size());
        assertNull(mockList.get(0));
    }
 
    @Test
    public void testSpyList() {
        //spy object will call the real method when not stub
        spyList.add("test");

        Mockito.verify(spyList).add("test");
        assertEquals(1, spyList.size());
        assertEquals("test", spyList.get(0));
    }
 
    @Test
    public void testMockWithStub() {
        //try stubbing a method
        String expected = "Mock 100";
        when(mockList.get(100)).thenReturn(expected);
 
        assertEquals(expected, mockList.get(100));
    }
 
    @Test
    public void testSpyWithStub() {
        //stubbing a spy method will result the same as the mock object
        String expected = "Spy 100";
        //take note of using doReturn instead of when
        doReturn(expected).when(spyList).get(100);
 
        assertEquals(expected, spyList.get(100));
    }
}

何时应该使用模拟(mock)或间谍(spy)? 如果您想要安全并避免调用外部服务,只想测试单元内部的逻辑,则使用mock。如果您想要调用外部服务并执行真实依赖关系的调用,或者简单地说,您想要像运行程序一样仅存根特定方法,则使用spy。这就是Mockito中spy和mock之间的区别。


好的回答,但它会抛出 verify() 在一个 mock 上的错误,并且除非您在 @Before 的 setUp() 方法中初始化列表,就像这里一样 mockList = mock(ArrayList.class); spyList = spy(ArrayList.class); 并删除这里建议的 mock 和 spy 注释,否则测试将不会运行。我已经测试过了,我的测试现在通过了。 - The_Martian
@The_Martian 抛出异常是因为在 Before setUp() 方法中没有调用 MockitoAnnotations.initMocks(this)。 - Andrew Chelix

38

简而言之:

@Spy@Mock在代码测试中被广泛使用,但开发人员在何时使用它们时易混淆,因此开发人员最终会使用@Mock来确保安全。

  • 当您想要仅外部地测试功能而不实际调用该方法时,请使用@Mock
  • 当您想要外部+内部测试功能并且实际调用该方法时,请使用@Spy

以下是我采取美国场景的例子。

选民可以根据VotersOfBelow21VotersOfABove21进行划分。

理想的出口民意调查显示特朗普将赢得选举,因为VotersOfBelow21VotersOfABove21都将投票给特朗普,并说“我们选举了特朗普总统

但这不是真实情况:

两个年龄组的选民都投票给特朗普,因为他们除了特朗普先生没有其他有效的选择。

那么如何测试呢?

public class VotersOfAbove21 {
public void weElected(String myVote){
  System.out.println("Voters of above 21 has no Choice Than Thrump in 20XX ");
}
}

public class VotersOfBelow21 {
  public void weElected(String myVote){
    System.out.println("Voters of below 21 has no Choice Than Thrump in 20XX");
  }
}

public class ElectionOfYear20XX {
  VotersOfAbove21 votersOfAbove21;
  VotersOfBelow21 votersOfBelow21;
  public boolean weElected(String WeElectedTrump){
    votersOfAbove21.weElected(WeElectedTrump);
    System.out.println("We elected President Trump ");

    votersOfBelow21.weElected(WeElectedTrump);
    System.out.println("We elected President Trump ");
    return true;
  }

}

现在,在前两个年龄组中,人们都表示他们没有比特朗普更好的选择。这明确意味着他们只是因为别无选择才投票给特朗普。

现在,ElectionOfYear20XX 表明特朗普获胜是因为这两个年龄组的人群以压倒性优势投票给了他。

如果我们使用 @Mock 测试 ElectionOfYear20XX,那么我们可能无法得到特朗普获胜的真正原因,我们将只是测试外部原因。

如果我们使用 @Spy 测试 ElectionOfYear20XX,那么我们将得到特朗普获胜的真正原因和外部的出口民调结果,即内在原因+外在原因。


我们的 ELectionOfYear20XX_Test 类:

@RunWith(MockitoJUnitRunner.class)
public class ELectionOfYear20XX_Test {

  @Mock
  VotersOfBelow21 votersOfBelow21;
  @Mock
  VotersOfAbove21 votersOfAbove21;
  @InjectMocks
  ElectionOfYear20XX electionOfYear20XX;
  @Test
  public void testElectionResults(){
    Assert.assertEquals(true,electionOfYear20XX.weElected("No Choice"));
  }

}

这应该仅输出逻辑测试结果,即外部检查:

We elected President Trump 
We elected President Trump 

使用@Spy来进行外部测试以及在实际方法调用中进行内部测试。

@RunWith(MockitoJUnitRunner.class)
public class ELectionOfYear20XX_Test {

  @Spy
  VotersOfBelow21 votersOfBelow21;
  @Spy
  VotersOfAbove21 votersOfAbove21;
  @InjectMocks
  ElectionOfYear20XX electionOfYear20XX;
  @Test
  public void testElectionResults(){
    Assert.assertEquals(true,electionOfYear20XX.weElected("No Choice"));
  }

}

输出:

Voters of above 21 has no Choice Than Thrump in 20XX 
We elected President Trump 
Voters of below 21 has no Choice Than Thrump in 20XX
We elected President Trump 

7
什么样的解释。 - Ashish Bansal

22

简短概述:

使用mock,它会为您创建一个基本的空壳实例。

List<String> mockList = Mockito.mock(ArrayList.class);

使用 spy,您可以针对现有实例进行部分模拟

List<String> spyList = Mockito.spy(new ArrayList<String>());

Spy的典型用例:类具有参数化构造函数,您希望首先创建对象。


22

mock 用于模拟类的 所有 方法。

spy 用于模拟 部分 方法,对于其余方法则需要进行实际调用。


简单好答案 - redd77

19

我在这里创建了一个可运行的例子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();

详细信息请查看上面的链接。


17

开始的最佳位置可能是mockito文档

一般来说,mockito mock允许您创建存根。

例如,如果该方法执行昂贵的操作,则可以创建存根方法。假设它获取数据库连接,从数据库检索一个值并将其返回给调用者。获取数据库连接可能需要30秒钟,会使您的测试执行变得缓慢到您可能会切换上下文(或停止运行测试)的程度。

如果您正在测试的逻辑不关心数据库连接,则可以使用存根替换该方法,该存根返回硬编码值。

mockito spy使您能够检查方法是否调用了其他方法。这在尝试对遗留代码进行测试时非常有用。

如果要测试通过副作用工作的方法,则应使用mockito spy。这会将调用委托给真实对象,并允许您验证方法调用、调用次数等。


12
我喜欢这个建议的简单性:
  • 如果您想保持安全,并避免调用外部服务,只想测试单元内的逻辑,则使用模拟对象 (Mock)
  • 如果您想要调用外部服务并执行实际依赖项的调用,或者仅仅是说,您想要按原样运行程序并只存根特定方法,则使用间谍对象(Spy)

来源: https://javapointers.com/tutorial/difference-between-spy-and-mock-in-mockito/

常见区别如下:
  • 如果您想直接存根依赖项的方法,则模拟(Mock)该依赖项。
  • 如果您想存根依赖项中的数据,使其所有方法返回所需的测试值,则间谍(Spy)该依赖项。

1
请注意,Spy 和 Mock 始终是用于依赖项,而不是用于受测系统。 - leo9r

8
有点晚了,但我感觉其他回答并没有很好地阐述间谍和模拟对象之间的差异。所以这里有一个小演示。
给定一个要测试的服务:
public class EmailService {

public String sendMail(String email) {
    return String.format("Email successfully sent to %s", email);
   }
}

现在我们可以使用四种不同的场景进行一些测试。

  1. 调用并存根mock
  2. 调用未存根的mock
  3. 调用未存根的spy
  4. 调用并存根spy

设置:

private final String testEmail = "randomuser@domain.com";
private final String success = "SUCCESS";
@Mock EmailService emailService;
@Spy EmailService emailServiceSpy;

测试:

@Test
@Description("When mock is called, we can return any response we like")
public void simpleTest1() {

    when(emailService.sendMail(testEmail)).thenReturn(success);
    assertEquals(success, emailService.sendMail(testEmail));
}

@Test
@Description("When mock is called but not stubbed, we receive a null value")
public void simpleTest2() {
    assertNull(emailService.sendMail(testEmail));
}

@Test
@Description("When a spy is called but not stubbed, the concrete impl is called")
public void simpleTest3() {
    assertTrue(emailServiceSpy.sendMail(testEmail).contains(testEmail));
}

@Test
@Description("When a spy is called and stubbed, stubbed value is returned")
public void simpleTest4() {
    when(emailServiceSpy.sendMail(testEmail)).thenReturn(success);
    assertEquals(success, emailServiceSpy.sendMail(testEmail));
}

如果没有设置规则,Mock 将返回空值,而 Spy 如果没有设置规则,将会调用具体类中实现的方法。

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