Mockito - @Spy vs @Mock

Posted

技术标签:

【中文标题】Mockito - @Spy vs @Mock【英文标题】: 【发布时间】:2015-04-02 10:54:55 【问题描述】:

Mockito - 我理解间谍调用对象的真实方法,而模拟调用双重对象的方法。除非有代码气味,否则还要避免间谍。 但是,间谍是如何工作的,我应该什么时候真正使用它们? 它们与模拟有何不同?

【问题讨论】:

mockito mock vs. spy 的可能重复项 Mocking vs. Spying in mocking frameworks的可能重复 【参考方案1】:

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

spy 用于模拟 一些 方法,而其余方法则必须进行实际调用。

【讨论】:

简单的好答案【参考方案2】:

简而言之:

@Spy@Mock 在代码测试中被大量使用,但开发人员在使用其中之一的情况下确实混淆了,因此开发人员最终使用@Mock 以确保安全。

如果您只想在外部测试功能,请使用@Mock 无需实际调用该方法。 当你想在外部测试功能时使用@Spy+ 在内部调用该方法。

以下是我以美国Election20xx为例。

选民可以按照VotersOfBelow21VotersOfABove21来划分。

The ideal Exit poll says that Trump will win the election because VotersOfBelow21 and VotersOfABove21 both will vote for trump saying "We elected President Trump "

但这不是真实的场景:

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

那么如何测试呢?

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 

【讨论】:

什么解释【参考方案3】:

我喜欢这个建议的简单性:

如果您想安全并避免调用外部服务,并且只想测试单元内部的逻辑,请使用 mock。 如果您想调用外部服务并调用真正的依赖项,或者简单地说,您想按原样运行程序并仅存根特定方法,则使用 spy

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

一个共同的区别是:

如果您想直接存根依赖项的方法,请模拟该依赖项。 如果您想对依赖项中的数据进行存根,以便其所有方法返回您需要的测试值,则Spy该依赖项。

【讨论】:

请注意,Spy 和 Mock 始终适用于依赖项,而不适用于被测系统。【参考方案4】:

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

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

【讨论】:

【参考方案5】:

两者都可以用来模拟方法或字段。不同之处在于,在 mock 中,您正在创建一个完整的 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?如果您想安全并避免调用外部服务并且只想测试单元内部的逻辑,请使用模拟。如果你想调用外部服务并执行真正依赖的调用,或者简单地说,你想按原样运行程序并且只是存根特定的方法,那么使用 spy.这就是 mockito 中 spy 和 mock 的区别。

【讨论】:

很好的答案,但它只会在模拟错误上抛出 verify() 并且不会运行测试,除非您在 @Before setUp() 方法中初始化列表,就像这里 mockList = mock(ArrayList.class ); spyList = spy(ArrayList.class);并删除此处建议的模拟和间谍注释。我已经测试过了,现在我的测试通过了。 @The_Martian 它正在抛出,因为在 setUp() 方法之前没有调用 MockitoAnnotations.initMocks(this)。【参考方案6】:

从技术上讲,“模拟”和“间谍”都是一种特殊的“测试替身”。

不幸的是,Mockito 使这种区别变得很奇怪。

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

mockito 中的间谍是其他模拟框架中的部分模拟(部分对象将被模拟,部分将使用真实的方法调用)。

【讨论】:

【参考方案7】:

TL;DR 版本,

使用 mock,它会为您创建一个简单的 shell 实例。

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

使用 spy,您可以部分模拟 现有 实例

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

Spy 的典型用例:类有一个参数化的构造函数,你想先创建对象。

【讨论】:

【参考方案8】:

最好的起点可能是the docs for mockito。

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

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

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

mockito spy 可以让您检查一个方法是否调用了其他方法。这在尝试测试遗留代码时非常有用。

如果您正在测试一种通过副作用起作用的方法,那么您将使用 mockito 间谍,这很有用。这会将调用委托给真实对象,并允许您验证方法调用、调用次数等。

【讨论】:

以上是关于Mockito - @Spy vs @Mock的主要内容,如果未能解决你的问题,请参考以下文章

用@spy模拟真实对象的部分行为

使用 Mockito 测试抽象类

使用 Mockito 测试抽象类

Mockito:试图监视方法正在调用原始方法

Mockito和PowerMock用法

VS下使用Microsoft Spy++工具