Mockito
前言
文章为个人总结存在一些问题还请大佬指教。
简介
是一个单元测试的模拟框架。通过设置模拟类达到模拟数据测试的轻框架。
依赖
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
如何编写一个好的测试代码?
1.测试代码应该保持和生产代码紧凑
2.测试代码各个依赖不应该强耦合,一些外部链接可以模拟
3.覆盖各种情况的出现
4.不应该模拟自己不拥有的类,例:第三方的代码
5.不应该模拟全部,否则不能达到测试真实代码的目的
Mockito的局限性
2.x版本(java 6+)
1.不能模拟构造方法
2.不能模拟静态方法
1.x版本(java 5+)
1.不能模拟final类
2.不能模拟final方法
3.不能模拟构造方法
4.不能模拟静态方法
快速入门
真实代码
@RestController
public class UserController {
@Resource
private UserService userService;
public User insertUser(User user){
...
}
测试代码
@RunWith(MockitoJUnitRunner.class)
public class UserControllerTest {
@InjectMocks
private UserController controller;
@Mock
private UserService userService;
@Test
public void insertUser() throws Exception {
User user = new User();
user.setId(5);
user.setUsername("张三");
user.setAge(18);
user.setPassword("567");
user.setTime(new Date());
doNothing().when(userService).insert(user);
controller.insertUser(user);
}
}
设置运行Mockito测试环境方式
1.@RunWith(MockitoJUnitRunner.class) 缺点在于当测试代码需要其他环境则无法进行
2.MockitoAnnotations.initMocks(this); 通常在@Before方法中指定,该方式可用于多个运行环境中测试
3.@Rule MockitoRule rule = MockitoJUnit.rule();
注入Mock对象
什么是mock对象
mock(模拟)对象,调用该对象的任何方法不会走真实方法逻辑,调用结果都相当于什么都不做,需自己通过stub来设置执行逻辑。
可以解决测试中不需要关心的/耦合强的/难以真实调用的对象方法,我们可以为它设置模拟数据返回。
例:1.代码中需要调用远程服务,而这里的逻辑不应该属于该单元测试中的范围,就可以模拟该远程服务对象返回模拟数据,保证正常测试执行。 2.又或是该测试代码我们不需要关心sql的执行情况,就可以模拟dao层对象返回测试数据
注入方式
1.@Mock
表明被注解对象是一个Mock(模拟)对象
2.mock(Class class);
UserService service = mock(UserService.class);
注入Spy对象
什么是spy对象
spy(监视)对象,与mock对象不同的是调用该对象会执行真实对象的代码逻辑,需自己通过stub来设置模拟逻辑。
注入方式
1.@Spy
表明被注解对象是一个Mock(模拟)对象
2.spy(Class class);
UserService service = spy(UserService.class);
@InjectMocks对象
会将其他的@Mock/@Spy修饰的对象注入到该注解修饰的对象。 此外使用该注解必须设置运行环境,例:MockitoAnnotations.initMocks(this);
注入逻辑
Mockito 将尝试仅通过构造函数注入、setter 注入或属性注入依次注入模拟。 如果以下任一策略失败,则 Mockito不会报告失败; 即必须自己提供依赖项。
构造函数注入;
选择最大的构造函数,然后使用仅在测试中声明的模拟来解析参数。 如果使用构造函数成功创建了对象,则Mockito 不会尝试其他策略。 Mockito 决定不破坏具有参数构造函数的对象。 注意:如果找不到参数,则传递 null。 如果需要不可模拟的类型,则不会发生构造函数注入。 在这些情况下,您必须自己满足依赖项。
属性设置器注入
mocks 将首先按类型解析,然后,如果有多个相同类型的属性,则通过属性名称和模拟名称的匹配。 注意1:如果你有相同类型的属性,最好用匹配的属性命名所有@Mock注解的字段,否则Mockito可能会混淆并且不会发生注入。 注意2:如果@InjectMocks 实例之前没有初始化并且有一个无参数构造函数,那么它将用这个构造函数初始化。
mock()/spy()注入
mocks 将首先按类型解析,然后,如果有多个相同类型的属性,则通过字段名称和模拟名称的匹配。 注意1:如果你有相同类型的字段,最好用匹配的字段命名所有@Mock注释的字段,否则Mockito可能会混淆并且不会发生注入。 注意2:如果@InjectMocks 实例之前没有初始化并且有一个无参数构造函数,那么它将用这个构造函数初始化。
多层级mock依赖注入解决
在日常开发中经常会出现这种情况:controllerA 注入了 serviceA,serviceA 注入了 daoA;
这种情况下如果我们想要在controllerA调用方法,然后当执行到daoA方法时进行模拟,mockito是无法完成的;
Mockito不支持为一个spy/mock注入另一个spy/mock。
解决
目前知道的解决方法仅有一个,有其他解决的方法希望大佬不吝赐教。
可以利用反射将serviceA的daoA属性换成mock的daoA属性。
@RunWith(MockitoJUnitRunner.class) //设置运行环境
public class UserControllerTest {
@InjectMocks //被测对象
private UserController controller;
@Spy //注入模拟对象
private UserService userService;
@Mock
private UserMapper userMapper;
@Before
public void setup(){
ReflectionTestUtils.setField(userService,"userMapper", this.userMapper);
}
@Test //方法测试
public void insertUser() throws Exception {
User user = new User();
user.setId(5);
user.setUsername("张三");
user.setAge(18);
user.setPassword("567");
user.setTime(new Date());
//表示当userService执行insert()方法时什么都不做
doNothing().when(userMapper).insert(user);
controller.insertUser(user);
}
}
Stub设置预设数据/逻辑
stub是为了让对象去执行的预设的一串逻辑,提供模拟的数据。
when(T methodCall)
表示当…,需要有参数,所以里面不能执行无返回值方法
thenReturn | Answer | Throw() | CallRealMethod
通常在when()后使用:when(list.get(0)).thenReturn(5); 表示当list执行了get(0)就返回5;
四个依次表示:然后返回 | 自定义逻辑返回 | 抛出个异常 | 回调真实方法
thenAnswer介绍
//使用自定义答案存根模拟的示例:
//Answer<T>:T返回值类型,Answer表示一个可获得Mock对象的接口
when(mock.someMethod(anyString())).thenAnswer(
new Answer() {
public Object answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return "called with arguments: " + Arrays.toString(args);
}
});
//stream流式编程
when(mock.someMethod(anyString())).thenAnswer(x->{
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return "called with arguments: " + Arrays.toString(args);
})
doReturn | Answer | Throw() | CallRealMethod
与thenXXX() 区别:
1.执行链路不一样
doReturn(5).when(list.get(0));
when(list.get(0)).thenReturn(5);
2.doXXX()可以作用于无返回值的方法
doReturn(5).when(list).add(5); //不报错
when(list.add(5)).thenReturn(5); //编译报错
3.对于spy对象建议使用doXXX(),因为spy对象执行方法是真实方法逻辑,使用when()会真实调用该方法而产生不希望出现的错误,如空指针异常等;
而我们通常使用stub就是为了预设数据不走真实方法,doXXX就是这样的逻辑。
参数匹配
any():匹配任何参数;anyInt():匹配任意int参数 …
when(list.get(anyInt())).doReturn(5);
Mock controller层请求进行测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class CustomerOrderControllerTest {
@Autowired
private CustomerOrderController controller;
private MockMvc mockMvc;
private WebApplicationContext applicationContext;
@Before
public void setupMockMvc(){
//参数指定controller则指对该controller获取其相关上下文
//mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
//读取整个web上下文构造Mock
mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext).build();
}
@Test
public void exceptionOrderBack() throws Exception {
String json = "{}";
mockMvc.perform(MockMvcRequestBuilders.post("/v1/customercenter/customerOrder/exceptionOrderBack")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content(json.getBytes())
).andExpect(MockMvcResultMatchers.status().isOk());
}
Springboot中进行JUnit Mockito单测
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
快速入门
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserControllerTest {
@Autowired
private UserController controller;
@SpyBean
private UserService userService;
@MockBean
private UserMapper userMapper;
@Before
public void setupMockMvc(){
ReflectionTestUtils.setField(userService,"userMapper", this.userMapper);
}
@Test
public void insertUser() throws Exception {
when(userMapper.getUserById(1l)).thenReturn(new User());
User user1 = controller.getUser(1l);
//不建议这种打印自己观察的方式进行单元测试,应采用Assert断言方式,这样能一目了然让我们知道测试是否通过
System.out.println(user1);
}
}
不同于Mockito本身,springboot整合后注入Mock对象采用的是 @SpyBean 和 @MockBean
区别是该注解是为了运行环境在SpringRunner/SpringJUnit4ClassRunner下的。
被@SpyBean 和 @MockBean注解的对象会在初始化时将原对象替换为该Mock对象,从而达到Mock的效果;但依旧存在多层级依赖注入问题,依旧采用反射设置属性的方式解决。
原因猜想
@SpyBean 和 @MockBean仅是替换了原来在Spring上下文的Bean,而这样其实经mock的serviceA中的daoA属性与替换的mock的daoA是没有关联的(相当于有两个对象,mockd的serviceA的daoA属性指向的并不是替换掉的mock(daoA))。
此外本人目前不清楚@SpyBeans和@MockBeans的使用,有了解者还希望能在评论区告知。
结尾
如果大家在使用中遇到过一些问题,欢迎一起讨论。
|