目录
一、简介
1.1 单元测试的特点
- 配合断言使用(杜绝System.out)
- 可重复执行
- 不依赖环境
- 不会对数据产生影响
- Spring的上下文环境不是必须得
- 一般都需要配合Mock类框架来实现的
1.2 Mock类框架的使用场景
要进行测试的方法存在外部依赖(如数据库,Redis,第三方接口调用等),为了能够专注对该方法或者单元的逻辑进行测试,就希望能够虚拟出外部依赖,避免外部依赖成为测试的阻塞项。
1.3 常见的Mock框架
Mock类框架:用于Mock外部依赖。
1.3.1 Mockito
官网文档:https://www.javadoc.io/doc/org.mockito/mockito-core/4.5.1/org/mockito/Mockito.html#13
限制:老版本对于final class、final method、statis method、private method均不能对Mockito mock,目前新版本已经支持final class、final method、statis method方法的mock,具体可以参考官网(有空了再补)
1.3.2 EasyMock
1.3.3 PowerMock
文档:https://github.com/powermock/powermock/wiki/Getting-Started
PowerMock
是一款功能十分强大的Mock工具,其基本语法与
Mockito
兼容,同时扩展了许多
Mockito
缺失的功能,包括对支持对私有、静态和构造方法实施Mock。但由于使用了自定义类加载器,会导致Jacoco在默认的
on-the-fly
模式下覆盖率跌零。
powerMock是基于easyMock或Mockito扩展出来的增强版本,所以powerMock分两种类型,如果你习惯于使用easyMock的,那你就下载基于easyMock的powerMock,反之你喜欢用mockito的话就下载另一种PowerMock。
但是好像也没有多少人用。。。
1.3.4 Testable
文档:https://alibaba.github.io/testable-mock/#/
TestableMock
现在已不仅是一款轻量易上手的单元测试Mock工具,更是以简化Java单元测试为目标的综合辅助工具集,与
PowerMock
基本平齐,且极易上手,只需掌握一个
@MockInvoke
注解就可以完成绝大多数Mock操作。
1.3.5 比较
工具原理最小Mock单元对被Mock方法的限制上手难度IDE支持Mockito动态代理类不能Mock私有方法较容易****很好PowerMock自定义类加载器类任何方法皆可较复杂较好JMockit运行时字节码修改类不能Mock构造方法(new操作符)较复杂一般TestableMock运行时字节码修改方法任何方法皆可较容易较好
二、Mockito的使用
2.1 导入pom文件
导入Mockito坐标和junit5的坐标
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.7.0</version><scope>compile</scope></dependency><dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>3.6.28</version><scope>compile</scope></dependency>
导入Mockito坐标和junit5的坐标,前期工作已经完成。
如果在springboot中我们还可以直接引用下面的坐标即可,其中的依赖已经包含了上述两者。
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><version>3.0.13</version><scope>test</scope></dependency>
2.2 mock对象和spy对象
方法类型方法插桩方法不插桩作用对象最佳实践mock对象执行插桩逻辑返回mock对象的默认值类、接口被测试类或者其他依赖spy对象执行插桩逻辑调用真实方法类、接口被测试类
- Mock不是真实的对象,它只是用类型的class创建了一个虚拟对象,并可以设置对象行为
- Spy是一个真实的对象,但它可以设置对象行为
2.3 初始化mock/spy对象的方式
测试版本方法一方法二方法三junit4@RunWith(MockitoJUnitRunner.class) + @Mock等注解MockitoAnnotations.initMocks(this);Mockito.mock(x.class)junit5@ExtendWith(MockitoExtension.class)+ @Mock等注解MockitoAnnotations.initMocks(this);Mockito.mock(x.class)
MockitoAnnotations.initMocks(this)方法已经被openMocks(this)替代。
我们现在来介绍一下初始化的三种方式:现阶段先关注初始化方法就行。。
importorg.junit.jupiter.api.Test;importorg.junit.jupiter.api.extension.ExtendWith;importorg.mockito.Mock;importorg.mockito.Mockito;importorg.mockito.Spy;importorg.mockito.junit.jupiter.MockitoExtension;importorg.junit.jupiter.api.BeforeEach;importorg.mockito.MockitoAnnotations;importorg.mockito.Mockito;/**
* 初始化mock/spy对象的第一种方式
*/@ExtendWith(MockitoExtension.class)publicclassTeacherServiceTestMethod1{@MockprivateTeacherService teacherService;@SpyprivateUserService userService;@Testpublicvoidtest1(){//Mockito.mockingDetails(teacherService).isMock() 用来判断该对象是不是一个mock的对象System.out.println(Mockito.mockingDetails(teacherService).isMock());System.out.println(Mockito.mockingDetails(userService).isSpy());System.out.println();}}/**
* 初始化mock/spy对象的第二种方式
*/classTeacherServiceTestMethod2{@MockprivateTeacherService teacherService;@SpyprivateUserService userService;@BeforeEachvoidsetUp(){//MockitoAnnotations.initMocks(this); 该方法已过时MockitoAnnotations.openMocks(this);}@Testpublicvoidtest1(){System.out.println(Mockito.mockingDetails(teacherService).isMock());System.out.println(Mockito.mockingDetails(userService).isSpy());System.out.println();}}/**
* 初始化mock/spy对象的第三种方式
*/classTeacherServiceTestMethod3{privateTeacherService teacherService;privateUserService userService;@BeforeEachvoidsetUp(){
userService =Mockito.mock(UserService.class);
teacherService =Mockito.spy(TeacherService.class);}@Testpublicvoidtest1(){System.out.println(Mockito.mockingDetails(teacherService).isMock());System.out.println(Mockito.mockingDetails(userService).isSpy());System.out.println();}}
我们随意找一个Test方法debug一下只要看到对象是这样就ok了。注意
$MockitoMock
就是说明模拟成功了
com.surpass.service.UserService$MockitoMock$1070386111@55e8ec2f
2.4 参数匹配
将参数匹配和方法插桩一起示例。。。
2.5 方法插桩
指定调用某个方法时的行为(stubbing),达到相互隔离的目的。
- 返回指定值
- void返回值方法插桩
- 插桩的两种方式 - doXxx().when(obj).method(); 其中obj可以使mock/spy对象- when(obj.method()).thenXxx();其中obj可以使mock对象
- 抛异常
- 多次插桩
- thenAnswer
- 执行真正的原始方法
- verify的使用
importstaticorg.mockito.ArgumentMatchers.any;importstaticorg.mockito.Mockito.doReturn;/**
* 参数匹配:通过方法签名(参数)来指定哪些方法调用时需要被处理(插桩、verify验证)
* 注意:在只用匹配器是要么都用要么都不用,禁止混搭!!举例
* 正确:getUserListByTeacher("name", "address")或者getUserListByTeacher(anyString(), anyString())
* 错误:getUserListByTeacher(anyString(), "address")
*/@ExtendWith(MockitoExtension.class)publicclassParamMatcherTest{@MockprivateTeacherService teacherService;/**
* 对于mock对象不会调用真实方法,直接返回mock对象的默认值
* 默认值(int)、null、空集合
*/@Testpublicvoidtest1(){TeacherEntity teacher = teacherService.getTeacherByName("");System.out.println("teacher = "+ teacher);Set<UserEntity> userListByTeacherName = teacherService.getUserListByTeacherName("");System.out.println("userListByTeacherName = "+ userListByTeacherName);}/**
* 方法插桩时的参数匹配
* 参数匹配时类(ArgumentMatchers)是匹配参数的主要成员,例如:
* any()则表示某一个方法传入任何类型都符合要求
* anyString()则表示字符串类型都符合要求
* !!!注意:所有的匹配都不包括null值
*/@Testpublicvoidtest2(){TeacherEntity teacherEntity =newTeacherEntity();
teacherEntity.setName("张三");
teacherEntity.setAddress("北京");//when(teacherService.getTeacherByName("")).thenReturn(teacherEntity); 此行插桩方式也可,此插桩意为当执行getTeacherByName方法是会返回之前创建好的对象teacherEntitydoReturn(teacherEntity).when(teacherService).getTeacherByName(any());TeacherEntity teacher = teacherService.getTeacherByName("123");System.out.println("teacher = "+ teacher);//验证校验teacherService.getTeacherByName()调用的次数,之前调了一次所以校验通过,如两次则抛异常verify(teacherService,times(1)).getTeacherByName(any());}}
控制台将打印如下内容
teacher =TeacherEntity(id=null, name=张三, address=北京, age=0)
其他项目的解释
@MockprivateList<String> mockList;/**
* 被插桩的方法在调用时不会执行实际的逻辑,直接返回指定的返回值
*/@Testpublicvoidtest3(){/**
* 指定返回值
*///方法插桩:当调用mockList.get(0)时返回指定返回值“zero”doReturn("zero").when(mockList).get(0);Assertions.assertEquals("zero", mockList.get(0));when(mockList.get(1)).thenReturn("one");Assertions.assertEquals("one", mockList.get(1));/**
* void返回值方法插桩
*/doNothing().when(mockList).clear();
mockList.clear();verify(mockList,times(1)).clear();/**
* 抛异常
*/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);}/**
* 多次插桩
* 意为第一次调用返回1,第二次调用返回2,第三次以及以后调用返回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());Assertions.assertEquals(3, mockList.size());}/**
* thenAnswer来实现对指定逻辑的插桩
*/@Testpublicvoidtest4(){when(mockList.get(anyInt())).thenAnswer((Answer<String>) invocation ->{//getArgument表示获取插桩方法(此处为mockList.get(anyInt()))的第几个参数值Integer argument = invocation.getArgument(0,Integer.class);returnString.valueOf(argument *100);});//执行get方法System.out.println(mockList.get(1));}/**
* 调用真实逻辑:控制台打印
* 进入方法: getTeacher
* TeacherEntity(id=1, name=123, address=北京市, age=-1166257546)
*/@Testpublicvoidtest5(){when(teacherService.getTeacher(any())).thenCallRealMethod();System.out.println(teacherService.getTeacher("123"));}
2.6 @InjectMocks注解的使用
- 作用:若此注解声明的变量需要用的mock/spy对象,mockito会自动将当前类里面的mock/spy对象注入到当中
- 原理:构造器注入、setter注入、字段反射注入
TeacherService实现
toString()
方法,调用
System.out.println(teacherService);
控制台打印如下内容:
TeacherService{teacherDao=teacherDao, userDao=userDao}
dubug能够看到两个Dao被自动注入到Service中去。
@ExtendWith(MockitoExtension.class)publicclassInjectMocksTest{/**
* 被InjectMocks注解标注的助兴必须是实现类,因为mockito会创建对应的实例对象
* 未经过mockito处理的普通对象会配合@spy注解使其变成默认调用真实方法的mock对象
* mockito会使用spy对象或mock对象注入到InjectMocks对应的实例对象中
*/@Spy@InjectMocksprivateTeacherService teacherService;@MockprivateTeacherDao teacherDao;@MockprivateUserDao userDao;@Testpublicvoidtest1(){System.out.println(teacherService);}}
2.7 断言工具
- hamcrest:junit4中引入的第三方断言库,junit5中被移除掉了。
- assertj:常用断言库。
- junit4原生断言库
- junit5原生断言库
junit5常用断言类库:
org.junit.jupiter.api.Assertions
。
版权归原作者 听风听雨听世界 所有, 如有侵权,请联系我们删除。