Mockito
文章目录
- Mockito
- 前言
- 简介
- 依赖
- 如何编写一个好的测试代码?
- Mockito的局限性
- 快速入门
- 真实代码
- 测试代码
- 设置运行Mockito测试环境方式
- 注入Mock对象
- 什么是mock对象
- 注入方式
- 注入Spy对象
- 什么是spy对象
- 注入方式
- @InjectMocks对象
- 注入逻辑
- 多层级mock依赖注入解决
- 解决
- Stub设置预设数据/逻辑
- 参数匹配
- Mock controller层请求进行测试
- Springboot中进行JUnit Mockito单测
- 依赖
- 快速入门
- 结尾
前言
文章为个人总结存在一些问题还请大佬指教。
简介
是一个单元测试的模拟框架。通过设置模拟类达到模拟数据测试的轻框架。
依赖
1
2
3
4
5
6
7<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.不能模拟静态方法
快速入门
真实代码
1
2
3
4
5
6
7
8@RestController public class UserController { @Resource private UserService userService; public User insertUser(User user){ ... }
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23@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()); //表示当userService执行insert()方法时什么都不做 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属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29@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介绍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//使用自定义答案存根模拟的示例: //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
2
3
4
5
6
7
8
91.执行链路不一样 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层请求进行测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24@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单测
依赖
1
2
3
4
5
6
7<!-- 该依赖中整合了mockito --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
快速入门
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22@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的使用,有了解者还希望能在评论区告知。
结尾
如果大家在使用中遇到过一些问题,欢迎一起讨论。
最后
以上就是着急枫叶最近收集整理的关于Mockito结合Junit单测使用的全部内容,更多相关Mockito结合Junit单测使用内容请搜索靠谱客的其他文章。
发表评论 取消回复