然而,我该如何使用Spy?它们与Mock有什么不同呢?
从技术上讲,“mocks”和“spies”都是一种特殊类型的“测试替身(test doubles)”。
但不幸的是,Mockito对此进行了奇怪的区分。
在其他模拟框架中,mockito中的"mock"是普通的mock(允许您存根调用;也就是说,从方法调用中返回特定的值)。
在其他模拟框架中,mockito中的"spy"是部分mock(对象的一部分将被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)? 如果您想要安全并避免调用外部服务,只想测试单元内部的逻辑,则使用mock。如果您想要调用外部服务并执行真实依赖关系的调用,或者简单地说,您想要像运行程序一样仅存根特定方法,则使用spy。这就是Mockito中spy和mock之间的区别。
简而言之:
@Spy
和@Mock
在代码测试中被广泛使用,但开发人员在何时使用它们时易混淆,因此开发人员最终会使用@Mock
来确保安全。
@Mock
。@Spy
。以下是我采取美国场景的例子。
选民可以根据VotersOfBelow21
和VotersOfABove21
进行划分。
理想的出口民意调查显示特朗普将赢得选举,因为VotersOfBelow21
和VotersOfABove21
都将投票给特朗普,并说“我们选举了特朗普总统”
但这不是真实情况:
两个年龄组的选民都投票给特朗普,因为他们除了特朗普先生没有其他有效的选择。
那么如何测试呢?
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
简短概述:
使用mock,它会为您创建一个基本的空壳实例。
List<String> mockList = Mockito.mock(ArrayList.class);
使用 spy,您可以针对现有实例进行部分模拟
List<String> spyList = Mockito.spy(new ArrayList<String>());
Spy的典型用例:类具有参数化构造函数,您希望首先创建对象。
mock 用于模拟类的 所有 方法。
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();
详细信息请查看上面的链接。
开始的最佳位置可能是mockito文档。
一般来说,mockito mock允许您创建存根。
例如,如果该方法执行昂贵的操作,则可以创建存根方法。假设它获取数据库连接,从数据库检索一个值并将其返回给调用者。获取数据库连接可能需要30秒钟,会使您的测试执行变得缓慢到您可能会切换上下文(或停止运行测试)的程度。
如果您正在测试的逻辑不关心数据库连接,则可以使用存根替换该方法,该存根返回硬编码值。
mockito spy使您能够检查方法是否调用了其他方法。这在尝试对遗留代码进行测试时非常有用。
如果要测试通过副作用工作的方法,则应使用mockito spy。这会将调用委托给真实对象,并允许您验证方法调用、调用次数等。
- 如果您想保持安全,并避免调用外部服务,只想测试单元内的逻辑,则使用模拟对象 (Mock)。
- 如果您想要调用外部服务并执行实际依赖项的调用,或者仅仅是说,您想要按原样运行程序并只存根特定方法,则使用间谍对象(Spy)。
来源: https://javapointers.com/tutorial/difference-between-spy-and-mock-in-mockito/
常见区别如下:
- 如果您想直接存根依赖项的方法,则模拟(Mock)该依赖项。
- 如果您想存根依赖项中的数据,使其所有方法返回所需的测试值,则间谍(Spy)该依赖项。
public class EmailService {
public String sendMail(String email) {
return String.format("Email successfully sent to %s", email);
}
}
现在我们可以使用四种不同的场景进行一些测试。
设置:
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));
}