一,简介
1.1 单元测试的特点
- 配合断言使用(杜绝 System.out )
- 可重复执行
- 不依赖环境
- 不会对数据产生影响
- spring 的上下文环境不是必须的
- 一般都需要配合 mock 类框架来实现
1.2 mock 类框架使用场景
要进行测试的方法存在外部依赖(如 db, redis, 第三方接口调用等), 为了能够专注于对该方法(单元)的逻辑进行测试,就希望能虚拟出外部依赖,避免外部依赖成为测试的阻塞项,一般都是测试 service 层即可。
1.3 常用 mock类框架
mock 类框架;用于 mock 外部依赖
1.3.1 mockito
名称:ito: input to output
官网文档: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
限制:老版本对于 fianl class, final method, static method, private method 均不能被 mockito mock, 目前已支持final class, final method, static method 的 mock, 具体可以参考官网
原理:bytebuddy, 教程: https://www.bilibili.com/video/BV1G24y1a7bd
1.3.2 easymock
1.3.3 powermock
与mockito的版本支持关系:https://gitee.com/mirrors/powermock/wikis/Mockito#supported-versions 对 mockito 或 easymock 的增强
1.3.4 JMockit
二,mockito 的单独使用
2.1 mock 对象与 spy 对象
方法插桩方法不插桩作用对象最佳实践mock 对象执行插桩逻辑返回mock对象的默认值类,接口被测试类或其他依赖spy 对象执行插桩逻辑调用真实方法类,接口被测试类
2.2 初始化 mock/spy 对象的方式
方法一方法二方法三junit4@RunWith(MockitoJUnitRunner.class) + @Mock等注解Mockito.mock(X.class)等静态方法MockitoAnnotations.openMocks(this)+@Mock等注解junit5@ExtendWith(MockitoExtension.class) + @Mock等注解
示例
/**
* 初始化 mock/spy 对象的方式有三种,第一种
* @author zhangdaowen
*/@ExtendWith(MockitoExtension.class)publicclassInitMockOrSpyMethod1{@MockprivateUserService mockUserService;@SpyprivateUserService spyUserService;@Testpublicvoidtest1(){// true 判断某对象是不是mock对象System.out.println(" "+Mockito.mockingDetails(mockUserService).isMock().isMOck());// falseSystem.out.println(""+Mockito.mockingDetails(mockUserService).isMock().isSpy());System.out.println(""+Mockito.mockingDetails(spyUserService).isMock());System.out.println(""+Mockito.mockingDetails(spyUserService).isMock());}}/**
* 初始化 mock/spy 对象的方式有三种,第二种
* @author zhangdaowen
*/publicclassInitMockOrSpyMethod2{privateUserService mockUserService;privateUserService spyUserService;@BeforeEachpublicvoidinit(){
mockUserService =Mockito.mock(UserService.class);
spyUserService =Mockito.spy(UserService.class);}@Testpublicvoidtest1(){// true 判断某对象是不是mock对象System.out.println(" "+Mockito.mockingDetails(mockUserService).isMock().isMOck());// falseSystem.out.println(""+Mockito.mockingDetails(mockUserService).isMock().isSpy());System.out.println(""+Mockito.mockingDetails(spyUserService).isMock());System.out.println(""+Mockito.mockingDetails(spyUserService).isMock());}}/**
* 初始化 mock/spy 对象的方式有三种,第三种
* @author zhangdaowen
*/publicclassInitMockOrSpyMethod3{@MockprivateUserService mockUserService;@SpyprivateUserService spyUserService;@BeforeEachpublicvoidinit(){MockitoAnnotations.openMocks(this);}@Testpublicvoidtest1(){// true 判断某对象是不是mock对象System.out.println(" "+Mockito.mockingDetails(mockUserService).isMock().isMOck());// falseSystem.out.println(""+Mockito.mockingDetails(mockUserService).isMock().isSpy());System.out.println(""+Mockito.mockingDetails(spyUserService).isMock());System.out.println(""+Mockito.mockingDetails(spyUserService).isMock());}}
2.3 参数匹配
/**
* 参数匹配; 通过方法签名(参数)来指定哪些方法调用需要被处理(插桩,verify验证)
* @author zhangdaowen
*/@ExtendWith(MockitoExtension.class)publicclassParamMatcherTest{@MockprivateUserService mockUserService;@Testpublicvoidtest4(){List<String> features =newArrayList<>();
mockUserService.add("乐之者Java", phone:"123", features);// 校验参数为 "乐之者Java", "123", features 的 add 方法调用了1次Mockito.verify(mockUserService,MOckito.times(wantedNumberOfInvocations:1)).add("乐之者Java", phone:"123", features);// 报错:When using matchers, aLL arguments have to be provided by matches//Mockito.verify(mockUserService,Mockito.times(wantedNumberOfInvocations:1)).add(ArgumentMatchers.anyString(), "123", features);Mockito.verify(mockUserService,Mockito.times(wantedNumberOfInvocations:1)).add(ArgumentMatchers.anyString(), anyString,anyList());}/**
* ArgumentMatchers.any 拦截 UserUpdateReq 类型的任意对象
* 除了any, 还有anyXX(anyLong, anyString) 注意:它们都不包括null
*/@Testpublicvoidtest3(){Mockito.doReturn(toBeReturned:99).when(mockUserService).modifyById(ArgumentMatchers.any(UserUpdateReq.class));UserUpdateReq userUpdateReq1 =newUserUpdateReq();
userUpdateReq1.setId(1L);
userUpdateReq1.setPhone("1L");Mockito.doReturn(toBeReturned:99).when(mockUserService).modifyById(userUpdateReq1);int result1 = mockUserService.modifyById(userUpdateReq1);UserUpdateReq userUpdateReq2 =newUserUpdateReq();
userUpdateReq2.setId(2L);
userUpdateReq2.setPhone("2L");Mockito.doReturn(toBeReturned:99).when(mockUserService).modifyById(userUpdateReq2);int result2 = mockUserService.modifyById(userUpdateReq2);}/**
* 测试插桩时的参数匹配, 只拦截userUpdateReq1
*/@Testpublicvoidtest2(){UserUpdateReq userUpdateReq1 =newUserUpdateReq();
userUpdateReq1.setId(1L);
userUpdateReq1.setPhone("1L");//指定参数为userUpdateReq1时调用mockUserService.modifyById返回99Mockito.doReturn(toBeReturned:99).when(mockUserService).modifyById(userUpdateReq1);// 此处并不产生结果int result1 = mockUserService.modifyById(userUpdateReq1);// 此处产生结果UserUpdateReq userUpdateReq2 =newUserUpdateReq();
userUpdateReq2.setId(2L);
userUpdateReq2.setPhone("2L");//指定参数为userUpdateReq1时调用mockUserService.modifyById返回99Mockito.doReturn(toBeReturned:99).when(mockUserService).modifyById(userUpdateReq2);int result2 = mockUserService.modifyById(userUpdateReq2);}/**
* 对于 mock 对象不会调用真实方法,直接返回 mock对象的默认值
* 默认值(int), null(UserVO), 空集合(List)
*/@Testpublicvoidtest1(){UserVO userVO = mockUserService.selectById(1);// nullSystem.out.println("userVO = "+ userVO);UserUpdateReq userUpdateReq1 =newUserUpdateReq();int i = mockUserService.modifyById(UserUpdateReq1);System.out.println("i="+ i);}}
2.4 方法插桩
指定调用某个方法时的行为(stubbing),达到相互隔离的目的
- 返回指定值
- void返回值方法插桩
- 插桩的两种方式- when(obj.someMethod()).thenXxx():其中 obj 可以是 mock 对象- doXxx(.when(obj).someMethod(): 其中 obj 可以是 mock/spy 对象 或者是无返回值的方法进行插桩
- 抛异常
- 多次插桩
- thenAnswer
- 执行真正的原始方法
- verify的使用
/**
* @author zhaodaowen
* @see <a href="http://www.roadjava.com">乐之者java</a>
*/@ExtendWith(MockitoExtension.class)publicclassStubTest{@MockprivateList<String> mockList;@MockprivateUserServiceImp1 mockUserServiceImp1;@psyprivateUserServiceImp1 spyUserServiceImp1;/**
* 测试verigy
*/@Testpublicvoidtest8(){
mockList.add("one");// true: 调用mock对象的写操作方法是没效果的Assertions.assertEquals(0, mockList.size());
mockList.clear();// 验证调用过1次add方法,且参数必须是oneverify(mockList)// 指定要验证的方法和参数,这里不是调用,也不会产生作用效果.add("oen");// 等价于上面的verigy(mockedList)verify(mockList,times(1)).clear();// 检验没有调用的两种方式verify(mockList,times(0)).clear();verify(mockList,never()).get(1);// 检验最少或最多调用了多少次verify(List,atLeast(1)).clear();verify(List,atMost(3)).clear();}/**
* 执行真正的原始方法
*/@Testpublicvoidtest7(){when(mockUserServiceImpl.getNUmber()).thenCallRealMethod();int number = mockUserServiceImp1.getNumber();Assertions.assertEquals(0, number);// spy对象默认就会调用真实方法,如果不想让它调用,需要单独为它进行插桩int spyResult = spyUserServiceImpl.getNumber();Assertions.assertEquals(0, spyResult);// 不让spy对象调用真实方法doReturn(1000).when(spyUserServiceImpl).getNumber();
spyResult = spyUserServiceImpl.getNumber();Assertions.assertEquals(1000, spyResult);}/**
* thenAnswer 实现指定逻辑的插桩
*/@TestpublicvoidTest6(){when(mockList.get(anyInt())).thenAnswer(newAnswer<String>(){/**
* 泛型表示要插桩方法的返回值类型
*/@OverridepublicStringanswer(InvocationOnMock invocation)throwsThrowable{// getArgument 表示获取插桩方法(此处就是List.get)的第几个参数值Integer argument = invocation.getArgument(0,Integer.class);returnString.vaLueOf(argument *100);}});String result = mockList.get(3);Assertions.assertEquals("300", result);}/**
* 多次插桩
*/@Testpublicvoidtest5(){// 第一次调用返回1, 第二次调用返回2, 第3次及之后的调用都返回3// when(mockList.size()).thenReturn(1).thenReturn(2).thenReturn(3);// 可间接写为when(mockList.size()).thenReturn(1,2,3);Assertions.assertEquals(1, mockList.size());Assertions.assertEquals(2, mockList.size());Assertions.assertEquals(3, mockList.size());}/**
* 抛出异常
*/@Testpublicvoidtest4(){// 方法一doThrow(RuntimeException.class).when(mockList).clear();try{
mockList.clear();// 走到下面这一行,说明插桩失败了Assertions.fail();}catch(Exception e){// 断言表达式为真Assertions.assertTrue(e instanceofRuntimeException);}// 方法二when(mockList.get(anyInt)).thenThrow(RuntimeException.class);try{
mockList.get(4);Assertions.fail();}catch(Exception e){Assertions.assertTrue(e instanceofRuntimeException);}}/**
* 插桩的两种方式
*/@Testpublicvoidtest3(){when(mockUserServiceImp1.getNumber()).thenReturn(99);// mockUserServiceImpl.getNumber() = 99 不调用真实方法System.out.println(""+ mockUserServiceImp1.getNumber());when(spyUserServiceImp1.getNumber()).thenReturn(99);// getNumber// spyUserServiceImpl.getNumber() = 99// spy对象在没有插桩时是调用真实方法的,写在when中会导致先执行一次原方法,达不到mock的目的// 需使用doXxx.when(obj).someMethod();其中obj可以是mock/spy对象System.out.println(""+ spyUserServiceImp1.getNumber());doReturn(1000).when(spyUserServiceImpl).getNumber();}/**
* void 返回值插桩
*/@Testpublicvoidtest2(){// 调用mockList.clear的时候什么也不做doNothing().when(mockList).clear();
mockList.clear();// 验证调用了一次clearverfy(mockList,times(wantedNumberOfInvocations:1)).clear();}/**
* 指定返回值
*/@TestpublicvoidTest1{// 方法一doReturn("zero").when(mockList).get(0);// 如果返回值不相等则本单元测试会失败Assertions.assertEquals("zero", mockList.get(0));//方法二when(mockList.get(1)).thenReturn("one");Assertions.assertEquals("one", mockList.get(1));}}
2.5 @InjectMocks注解的使用
- 作用:若@InjectMocks 声明的变量需要用到 mock/spy 对象,mockito 会自动使用当前类里的 mock 或 spy 成员进行按类型或名字的注入
- 原理:构造器注入、setter注入、字段反射注入
@ExtendWith(MockitoExtension.class)publicclassInjectMocksTest{/**
* 1.被@InjectMocks标注的属性必须是实现类,因为mockito会创建对应的示例对象,默认创建的对象就是未经过mockito处理的普通对象,因此常配合@Spy注解使其变为默认调用真实方法的mock对象
* 2.mockito会使用spy或mock对象注入到@InjectMocks对应的示例对象中
*/@Spy@InjectMocksprivateUserService userService;@MockprivateUserFeatureService userFeatureService;@MockprivateList<String> mockList;@Testpublicvoidtest1(){int number = userService.getNumber();Assertions.assertEquals(0, number);}}
2.6 断言工具
namcrest:junit4 中引入的第三方断言库,junit5 中被移出,从 1.3 版本后,坐标由 org.hamcrest:hamcrest 变为org.hamcrest:hamcrest
assert: 常用的断言库
junit4 原生断言
junit5 原生断言
@ExtendWith(MockitoExtension.class)publicclassAssertTest{@MockprivateList<String> mockList;@Testpublicvoidtest1(){when(mockList.size()).thenReturn(999);// 测试hamcrest的断言MatchAssert.assertThat(mockList.size(),IsEqual.equalTo(999));// 测试 assertJ assertThat: 参数为实际的值Assertions.assertThat(mockList.size().isEquaTo(999));// junit5原生断言orj.junit.jupiter.api.Assertions.assertEquals(999, mockList.size());// junit4原生断言org.junit.Assert.assertEquals(999, mockList.size());}}
三,实战讲解
四,mockito在springboot环境使用(不推荐-)
生成的对象受 spring 管理,相当于自动替换对应类型 bean 的注入
@MockBean
- 类似@Mock
- 用于通过类型或名字 替换 spring 容器中已经存在的bean,从而达到对这些bean进行mock的目的
@SpyBean
- 作用类似@Spy
- 用于通过类型或名字包装spring容器中已经存在的bean,当需要mock被测试类的某些方法时可以使用
/**
* Mock配合spring使用
**/@SpringBootTest(class=MockitoApp.class)publicclassUserServiceImplInSpringTest{/**
* 不能配置@Spy: Argument passed to when() is not mock!
*/@SpyBean@ResourceprivateUserServiceImp1 userService;@MockprivateUserFeatureService userFeatureService;@MockprivateUserMapper userMapper;@TestpublicvoidtestSelectById3(){//配置方法getById的返回值UserDo ret =newUserDo();
ret.setId(1L);
ret.setUsername("乐之者java");
ret.setPhone("http://www.roadjava.com");doReturn(ret).when(userService).getById(1L);//配置userFeatureService.seLectByUserId的返回值List<UserFeatureDO> userFeatureDoList =newArrayList<>();UserFeatureDO userFeatureDO =newUserFeatureDo();
userFeatureD0.setId(88L);
userFeatureD0.setUserId(1L);
userFeatureDO.setFeatureValue("aaaa");
userFeatureDoList,add(userFeatureDo);doReturn(userFeatureDoList).when(userFeatureService).selectByUserId(lL);// 执行测试UserVo userVO = userService,selectById(userld:1L);// 断言Assertions.assertEquals(expected:1,userVO.getFeatureValue().size());}@TestpublicvoidtestSelectById2(){// 配置方法getById的返回值UserDo ret =newUserDo();
ret.setId(1L);
ret.setUsername("乐之者java");
ret.setPhone("http://www.rodajava.com");// up主的广告doReturn(ret).when(userService).getById(1L);UserVo userVO = userService.selectById(1L);Assertions.assertNotNUll(userVO);}@TestpublicvoidtestSelectById1(){// 配置doReturn(userMapper).when(userService).getBaseMapper();UserVO userVO = userService.selectById(1L);Assertions.assertNull(userVO);}}
漫谈
版权归原作者 陈逸轩*^_^* 所有, 如有侵权,请联系我们删除。