前言
-
Mock
是将目标对象整个模拟 ,所有方法默认都返回null
,并且原方法中的代码逻辑不会执行,被Mock
出来的对象,想用哪个方法,哪个方法就需要打桩,否则返回null
; -
Spy
可实现对目标对象部分方法、特定入参条件时的打桩,没有被打桩的方法,将会真实调用。
本文maven依赖
本文使用了
Spring
、Junit5
、Mokito
、commons-lang3
工具类。
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
1. 非Spring环境
假设我们有如下服务:
public class DemoService {
public String serviceA(String name) {
System.out.println("serviceA 被真实调用了,当前入参:" + name);
return "serviceA 真实返回:" + name;
}
public String serviceB(String name) {
System.out.println("serviceB 被真实调用了,当前入参:" + name);
return "serviceB 真实返回:" + name;
}
}
我们用如下单测代码示例,注释已经很清楚了,此处不再赘述。
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
class DemoServiceTest {
@Test
@DisplayName("Mock的正确姿势")
void testMock() {
// Mock 出一个目标对象的实例,注意不是 new 出来的真实对象
DemoService mockService = Mockito.mock(DemoService.class);
// 假设我们仅对 serviceA 方法,并且入参等于 "asdf" 时进行打桩
Mockito.when(mockService.serviceA(Mockito.eq("asdf")))
.thenReturn("mock:serviceA方法入参等于asdf时的特定返回值");
// 由于上边已经打桩了,并且这个调用正好命中打桩规则,因次返回值将是 "mock:serviceA方法入参等于asdf时的特定返回值"
String asdf = mockService.serviceA("asdf");
System.out.println("asdf = " + asdf);
Assertions.assertEquals("mock:serviceA方法入参等于asdf时的特定返回值", asdf);
// 虽然也是调用 serviceA 方法,但由于没有命中打桩规则,所以返回值是 null
String qwer = mockService.serviceA("qwer");
System.out.println("qwer = " + qwer);
Assertions.assertNull(qwer);
// serviceB 根本没有打桩,但由于 mockService 这个对象实例是 Mock 出来的,
// 所以 serviceB 的方法体代码不会被执行,并且返回值固定为 null,不管入参是什么
for (int i = 0; i < 3; i++) {
String returnValueFromRandom = mockService.serviceB(RandomStringUtils.random(10));
System.out.println("returnValueFromRandom 第[" + i + "]次 = " + returnValueFromRandom);
Assertions.assertNull(returnValueFromRandom);
}
}
@Test
@DisplayName("Spy的错误打桩姿势")
void testBadSpy() {
// Spy 一个目标对象的实例,注意不是 new 出来的 100% 真实的实例,也不是 100% 假的实例
// 到底有多真,有多假,取决于打桩埋点的覆盖程度
DemoService spyService = Mockito.spy(DemoService.class);
// 打桩错误示例:参照 Mock 打桩的写法,预期对 serviceA 方法打桩,
// 并且当入参等于 "asdf" 时,返回特定值 "spy:serviceA方法入参等于asdf时的特定返回值"
Mockito.when(spyService.serviceA(Mockito.eq("asdf")))
.thenReturn("spy:serviceA方法入参等于asdf时的特定返回值");
// 由于上边已经打桩了,并且这个调用正好命中打桩规则,因次返回值将是 "spy:serviceA方法入参等于asdf时的特定返回值"
String asdf = spyService.serviceA("asdf");
System.out.println("asdf = " + asdf);
Assertions.assertEquals("spy:serviceA方法入参等于asdf时的特定返回值", asdf);
// 注意:上述对入参等于 "asdf" 时,被"spy错误打桩"的方法 serviceA 的返回值的断言,是可以跑通的
// 但是,存在如下两个问题:
// 1. serviceA 被真实调用了;(这一点往往不是我们的预期)
// 2. serviceA 被真实调用时,它的实际入参,其实并不是 "asdf",而是 null。
// (想象一下如果 serviceA 方法体内真正运行时,极有可能由于实际入参是 null 而抛出异常中断执行,这是一种灾难)
// 下述调用,虽然 serviceA 被打桩,由于 "qwer" 并没有命中打桩点,所以它不会返回打桩点设定的返回值
// 不同于 Mock 出来的对象,下述调用会真实地执行 serviceA 方法体代码块,而不是像 Mock 一样返回 null
System.out.println("==============================================");
String qwer = spyService.serviceA("qwer");
System.out.println("qwer = " + qwer);
Assertions.assertEquals("serviceA 真实返回:qwer", qwer);
// 同理,serviceB 方法没有被打桩,但由于 spyService 是 Spy 出来的对象实例,
// 不同于 Mock 出来的对象实例返回 null,对 serviceB 的调用会真实的执行目标方法,并按真实情况返回
System.out.println("==============================================");
String zxcv = spyService.serviceB("zxcv");
System.out.println("zxcv = " + zxcv);
Assertions.assertEquals("serviceB 真实返回:zxcv", zxcv);
}
@Test
@DisplayName("Spy的正确打桩姿势")
void testGoodSpy() {
// 同样,先用 Spy 方式创建一个目标对象的实例
DemoService spyService = Mockito.spy(DemoService.class);
// 对 Spy 对象的正确打桩姿势,
// (巧了,用这种 doReturn|doThrow|doAnswer(xxx).when(obj).methodXXX() 的方式,也适用于 Mock 出来的对象)
Mockito.doReturn("spy:serviceA方法入参等于asdf时的特定返回值2")
// 注意这里 when 里面是对象实例的变量名,而不是一个 methodCall
.when(spyService)
// 再通过 when 泛型方法返回的Spy实例引用,对目标方法打桩
.serviceA(Mockito.eq("asdf"));
// 由于上边已经打桩了,并且这个调用正好命中打桩规则,因次返回值将是 "spy:serviceA方法入参等于asdf时的特定返回值2"
String asdf = spyService.serviceA("asdf");
System.out.println("asdf = " + asdf);
Assertions.assertEquals("spy:serviceA方法入参等于asdf时的特定返回值2", asdf);
// 说明:由于上面正确的打桩姿势,上述被命中打桩规则的调用,不会触发目标方法的真实调用,不存在上述错误用法的2个问题。
// 对于调用打桩方法,但没有命中打桩入参条件的调用,表现行为与上述 testBadSpy 单测代码一样
System.out.println("==============================================");
String qwer = spyService.serviceA("qwer");
System.out.println("qwer = " + qwer);
Assertions.assertEquals("serviceA 真实返回:qwer", qwer);
// 同理,对于没有打桩的 spy 对象实例的其他方法,表现行为与上述 testBadSpy 单测代码一样
System.out.println("==============================================");
String zxcv = spyService.serviceB("zxcv");
System.out.println("zxcv = " + zxcv);
Assertions.assertEquals("serviceB 真实返回:zxcv", zxcv);
}
}
2. Spring环境
假设我们有如下服务:
import org.springframework.stereotype.Component;
@Component
public class DemoBeanService {
public String serviceA(String name) {
System.out.println("bean serviceA 被真实调用了,当前入参:" + name);
return "bean serviceA 真实返回:" + name;
}
public String serviceB(String name) {
System.out.println("bean serviceB 被真实调用了,当前入参:" + name);
return "bean serviceB 真实返回:" + name;
}
}
注意:由于
Spring IoC
默认是单例的,为了区分,我们分别为@MockBean
和@SpyBean
创建单独的测试类举例。
@MockBean示例
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ContextConfiguration;
@ContextConfiguration(classes = DemoBeanService.class) // 此处我们的IoC只扫描注入 DemoBeanService 类
@SpringBootTest
public class MockBeanTest {
@MockBean
DemoBeanService mockBeanService;
@Test
@DisplayName("MockBean的正确姿势")
void testMock() {
// 不同于普通 Mock 方式,这里不需要调用 Mockito.mock(xxx.class) 创建 mock 对象实例
// 我们已经通过 @MockBean 的方式,将一个 mock 对象的实例放入了 Spring IoC ApplicationContext 中
// 注意到 Spring IoC 默认是单例的,也就是当前 ApplicationContext 中,只有一个 mock 出来的 DemoBeanService 实例
// 所以,如果它打桩不全的话,在当前这个 IoC 中,调用没有被打桩的方法,将一律返回 null
// 因此在不同的 xxxTest.java 中出现不同 MockBean 时,会触发 Spring 上下文重建,写的 MockBean 越多,整个工程单测就越慢
// 假设我们仅对 serviceA 方法,并且入参等于 "asdf" 时进行打桩
Mockito.when(mockBeanService.serviceA(Mockito.eq("asdf")))
.thenReturn("MockBean:serviceA方法入参等于asdf时的特定返回值");
// 由于上边已经打桩了,并且这个调用正好命中打桩规则,因次返回值将是 "MockBean:serviceA方法入参等于asdf时的特定返回值"
String asdf = mockBeanService.serviceA("asdf");
System.out.println("asdf = " + asdf);
Assertions.assertEquals("MockBean:serviceA方法入参等于asdf时的特定返回值", asdf);
// 虽然也是调用 serviceA 方法,但由于没有命中打桩规则,所以返回值是 null
String qwer = mockBeanService.serviceA("qwer");
System.out.println("qwer = " + qwer);
Assertions.assertNull(qwer);
// serviceB 根本没有打桩,但由于 mockBeanService 这个对象实例是 Mock 出来的,
// 所以 serviceB 的方法体代码不会被执行,并且返回值固定为 null,不管入参是什么
for (int i = 0; i < 3; i++) {
String returnValueFromRandom = mockBeanService.serviceB(RandomStringUtils.random(10));
System.out.println("returnValueFromRandom 第[" + i + "]次 = " + returnValueFromRandom);
Assertions.assertNull(returnValueFromRandom);
}
}
}
@SpyBean示例
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.ContextConfiguration;
@ContextConfiguration(classes = DemoBeanService.class)
@SpringBootTest
public class SpyBeanTest {
@SpyBean
DemoBeanService spyBeanService;
@Test
@DisplayName("SpyBean的错误打桩姿势")
void testBadSpyBean() {
// 与 MockBean 类似,此处也不再需要 Mockito.spy(xxx.class) 创建 spy 对象实例
// IoC 中也同样有一个 spy 对象实例,详情参见 @MockBean 的Test示例
// 打桩错误示例:参照 Mock 打桩的写法,预期对 serviceA 方法打桩,
// 并且当入参等于 "asdf" 时,返回特定值 "SpyBean:serviceA方法入参等于asdf时的特定返回值"
Mockito.when(spyBeanService.serviceA(Mockito.eq("asdf")))
.thenReturn("SpyBean:serviceA方法入参等于asdf时的特定返回值");
// 由于上边已经打桩了,并且这个调用正好命中打桩规则,因次返回值将是 "SpyBean:serviceA方法入参等于asdf时的特定返回值"
String asdf = spyBeanService.serviceA("asdf");
System.out.println("asdf = " + asdf);
Assertions.assertEquals("SpyBean:serviceA方法入参等于asdf时的特定返回值", asdf);
// 注意:上述对入参等于 "asdf" 时,被"spy错误打桩"的方法 serviceA 的返回值的断言,是可以跑通的
// 但是,存在如下两个问题:
// 1. serviceA 被真实调用了;(这一点往往不是我们的预期)
// 2. serviceA 被真实调用时,它的实际入参,其实并不是 "asdf",而是 null。
// (想象一下如果 serviceA 方法体内真正运行时,极有可能由于实际入参是 null 而抛出异常中断执行,这是一种灾难)
// 下述调用,虽然 serviceA 被打桩,由于 "qwer" 并没有命中打桩点,所以它不会返回打桩点设定的返回值
// 不同于 Mock 出来的对象,下述调用会真实地执行 serviceA 方法体代码块,而不是像 Mock 一样返回 null
System.out.println("==============================================");
String qwer = spyBeanService.serviceA("qwer");
System.out.println("qwer = " + qwer);
Assertions.assertEquals("bean serviceA 真实返回:qwer", qwer);
// 同理,serviceB 方法没有被打桩,但由于 spyService 是 Spy 出来的对象实例,
// 不同于 Mock 出来的对象实例返回 null,对 serviceB 的调用会真实的执行目标方法,并按真实情况返回
System.out.println("==============================================");
String zxcv = spyBeanService.serviceB("zxcv");
System.out.println("zxcv = " + zxcv);
Assertions.assertEquals("bean serviceB 真实返回:zxcv", zxcv);
}
@Test
@DisplayName("SpyBean的正确打桩姿势")
// 注意:由于和 testBadSpyBean() 方法是分别打桩的(都是在各自的方法体局部打桩),因此不会互相影响
void testGoodSpyBean() {
// 当前测试方法(testGoodSpyBean),不使用 Mockito.spy(xxx.class) 创建 spy 对象实例
// 同时,由于和 testBadSpyBean() 在一个测试类中,会共用同一个 spy 对象实例(DemoBeanService的instance)
// 对 Spy 对象的正确打桩姿势,
// (巧了,用这种 doReturn|doThrow|doAnswer(xxx).when(obj).methodXXX() 的方式,也适用于 Mock 出来的对象)
Mockito.doReturn("SpyBean:serviceA方法入参等于asdf时的特定返回值3")
// 注意这里 when 里面是对象实例的变量名,而不是一个 methodCall
.when(spyBeanService)
// 再通过 when 泛型方法返回的Spy实例引用,对目标方法打桩
.serviceA(Mockito.eq("asdf"));
// 由于上边已经打桩了,并且这个调用正好命中打桩规则,因次返回值将是 "SpyBean:serviceA方法入参等于asdf时的特定返回值3"
String asdf = spyBeanService.serviceA("asdf");
System.out.println("asdf = " + asdf);
Assertions.assertEquals("SpyBean:serviceA方法入参等于asdf时的特定返回值3", asdf);
// 说明:由于上面正确的打桩姿势,上述被命中打桩规则的调用,不会触发目标方法的真实调用,不存在上述错误用法的2个问题。
// 对于调用打桩方法,但没有命中打桩入参条件的调用,表现行为与上述 testBadSpy 单测代码一样
System.out.println("==============================================");
String qwer = spyBeanService.serviceA("qwer");
System.out.println("qwer = " + qwer);
Assertions.assertEquals("bean serviceA 真实返回:qwer", qwer);
// 同理,对于没有打桩的 spy 对象实例的其他方法,表现行为与上述 testBadSpy 单测代码一样
System.out.println("==============================================");
String zxcv = spyBeanService.serviceB("zxcv");
System.out.println("zxcv = " + zxcv);
Assertions.assertEquals("bean serviceB 真实返回:zxcv", zxcv);
}
}
补充
- 其实,
@MockBean
和@SpyBean
注解,除了在FIELD
上可以支持外,也可以直接在测试类上使用,适用范围可参考上述注解的源码@Target({ ElementType.TYPE, ElementType.FIELD })
。因为这个漂亮的特性,我们可以通过设计一个测试基类BaseTest
,让它的所有测试子类,最大程度地共享同一个IoC
容器,而不是频繁触发重建ApplicationContext
拖慢执行速度,从而提高整个工程的单测执行效率。 - 此外,在
JAVA8
以前,是可以使用@MockBeans
或@SpyBeans
在同一个类上添加多个对应的@MockBean
或@SpyBean
,在JAVA8
以后,得益于@Repeatable
注解,您可以直接在一个类上添加n
个单一注解@MockBean
或@SpyBean
。