Mockito-@间谍vs @Mock


98

Mockito-我知道间谍在对象上调用真实方法,而模拟对象在double对象上调用方法。除非有代码气味,否则也应避免间谍活动。但是,间谍如何工作,我什么时候应该实际使用它们?它们与模拟游戏有何不同?



Answers:


88

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

不幸的是,莫克托(Mockito)的区分很奇怪。

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

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


40

两者都可以用来模拟方法或字段。区别在于,在模拟中,您创建的是一个完整的模拟或假对象,而在间谍中,则存在真实的对象,而您只是在监视或存根特定的方法。

当然,在间谍对象中,由于它是一个真实的方法,所以当您不对方法进行存根时,它将调用真实的方法行为。如果要更改和模拟该方法,则需要对它进行存根。

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

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

什么时候应该使用模拟或间谍?如果您想确保安全并避免调用外部服务,而只想测试单元内部的逻辑,则可以使用模拟。如果您想调用外部服务并执行实际依赖项的调用,或者简单地说,您想按原样运行该程序并仅存入特定方法,则可以使用spy。这就是模仿和模仿中的间谍与模仿之间的区别。


好的答案,但是它将在纯模拟错误上抛出verify(),并且除非您在@Before setUp()方法中初始化列表,否则将无法运行测试,就像此处的方法一样:模拟列表=模拟(ArrayList.class); spyList = spy(ArrayList.class); 并删除此处建议的模拟和间谍注释。我已经测试过了,现在测试通过了。
The_Martian '19

17

TL; DR版本,

使用模拟,它将为您创建一个准系统的shell实例。

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

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

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

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


14

我在这里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();
    }

然后,您可以像这样使用间谍注入依赖项:

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();

在上面的链接中有更多详细信息。


13

最好的起点可能是嘲笑文档

总的来说,mockito模拟允许您创建存根。

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

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

模拟间谍可以让您检查一个方法是否调用其他方法。尝试对遗留代码进行测试时,这可能非常有用。

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


7

简而言之:

@Spy并且@Mock在代码测试中被大量使用,但是在使用其中一种代码的情况下,开发人员确实会@Mock感到困惑,因此开发人员最终会出于安全考虑而使用。

  • 使用@Mock时,你只想测试功能 ,而不实际调用该方法。
  • @Spy当您要使用调用的方法从外部+内部测试功能时使用。

以下是我在美国采用Election20xx方案的示例。

选民可以根据VotersOfBelow21和划分VotersOfABove21

理想的出口民调说,特朗普将在竞选中获胜,因为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 说特朗普赢了,因为两个年龄段的人都以压倒性多数投票支持他。

如果我们要ElectionOfYear20XX使用@Mock测试,则可能无法获得特朗普获胜的真正原因,我们将仅测试外部原因。

如果我们ElectionOfYear20XX使用@Spy进行测试,那么我们就能得出特朗普赢得外部退出民意测验结果(即内部+外部)的真正原因。


我们的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 

6

我喜欢此建议的简单性:

  • 如果您想确保安全并避免调用外部服务,而只想测试单元内部的逻辑,则可以使用模拟
  • 如果您想调用外部服务并执行实际依赖项的调用,或者简单地说,您想按原样运行该程序并仅存入特定方法,则可以使用spy

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

共同的区别是:

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

请注意,Spy和Mock始终用于依赖项,而不用于受测系统。
leo9r
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.