单元测试及测试覆盖率报告生成
一般在开发中我们建议对于新写的业务逻辑进行单元测试,而不是将所有代码都写完之后再进行测试,这样既不能保证代码的可用性而且后续测试也会比较困难,因此掌握基本的测试API对于开发人员来说也是非常重要的,下面将简单的讲解一下我们开发中可能会用到的一些测试方面的知识。
junit5驱动和断言
和Junit4相比,Junit5框架更多在向测试平台演进。其核心组成也从以前的一个Junit的jar包更换成由多个模块组成。本文所需要依赖模块如下:
- junit-jupiter-engine: Junit的核心测试引擎
- junit-jupiter-params: 编写参数化测试所需要的依赖包
- junit-platform-launcher: 从IDE(InteliJ/Eclipses)等运行时所需要的启动器
依赖引入
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api --><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>5.6.2</version><scope>test</scope></dependency>
驱动&断言
对被测对象的执行过程称为驱动driver,而验证结果是否正确则称之为断言。可以简单理解为执行被测对象是驱动,验证结果是断言。
以下为常见的单元测试注解:
方法名描述@BeforeAll只执行一次,在所有测试和@BeforeEach注解方法之前执行@BeforeEach在每个测试执行之前执行@AfterEach在每个测试执行之后执行@AfterAll只执行一次,在所有测试和@AfterEach注解方法之后执行
注意:在测试执行前后会进行一些初始化和销毁的操作,因为框架会为每个测试创建一个单独的实例,所以在**@BeforeAll/@AfterAll方法执行时尚无任何测试实例诞生。因此,这两个方法必须定义为静态方法**。
数据驱动
使用@ParameterizedTest注解来实现数据驱动测试,通过一个用例的多组数据从而快速实现
/**
* 数据驱动使用
* @author lilei
*/publicclassDataDriverTest{/**
* 基础数据数据结构驱动
*
* @param words 单词
*/@ParameterizedTest@ValueSource(strings ={"Radar","Rotor","Tenet","Madam","Racecar"})voidmultiNameTest(String words){System.out.println(words);}/**
* CSV数据结构驱动
*
* @param startDate 开始日期
* @param endDate 结束日期
*/@ParameterizedTest@CsvSource({"2017-06-01,2018-10-15","2017-05-01,2018-10-15","2017-06-01,2018-11-16"})voidshouldCreateValidDateRange(LocalDate startDate,LocalDate endDate){System.out.println(startDate +"|"+ endDate);}/**
* 读取csv文件实现数据驱动
* @param country 国家
* @param reference 关联行数
*/@ParameterizedTest@CsvFileSource(resources ="/country.csv",numLinesToSkip =1)voidCsvFileSourceTest(String country,String reference){System.out.println(country+"-"+reference);}/**
* 方法数据源
* @param fruitName 水果
* @param num 数字
* @param list 集合
*/@ParameterizedTest@MethodSource("stringIntAndListProvider")voidtestWithMultiArgMethodSource(String fruitName,int num,List<String> list){assertEquals(5, fruitName.length());assertTrue(num >=1&& num <=2);assertEquals(2, list.size());}staticStream<Arguments>stringIntAndListProvider(){returnStream.of(arguments("apple",1,Arrays.asList("a","b")),arguments("lemon",2,Arrays.asList("x","y")));}}
基于Spring的单元测试
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration({"classpath:config-init.xml"})publicclassSpringTest{@AutowiredMsgSendLogMapper msgSendLogMapper;@TestpublicvoidspringConfigTest(){// do something....}}
基于SpringtBoot的单元测试
Service测试
@SpringBootTestpublicclassUserServiceImplTest{@ResourceprivateUserService userService;@Test@Transactional@Rollback(false)//是否进行回滚voidtest1(){User myu=newUser();
myu.setId(13);
myu.setUsername("red13");
myu.setPassword("passw0ld");
myu.setDemo("testjunit");this.userService.insert(myu);Assertions.assertEquals(this.userService.queryById(13).getId().toString(),"13");// this.userService.deleteById(12);}
Controller测试
@ExtendWith(SpringExtension.class)@WebMvcTest(DouyinContractController.class)classDouyinContractControllerTest{@AutowiredprivateMockMvc mockMvc;@MockBeanprivateIDouYinContractService mockContractService;@TestvoidtestBindCard()throwsException{// Setup// Configure IDouYinContractService.preContract(...).finalDouyinPreContractRes preContractRes =newDouyinPreContractRes();
preContractRes.setPreEntrustwebId("preEntrustwebId");
preContractRes.setCode("code");
preContractRes.setMessage("message");finalPreContractRes res =newPreContractRes(preContractRes);finalPreContractReq preContractReq =newPreContractReq();finalModelsUserInfo userInfo =newModelsUserInfo();
userInfo.setUserName("userName");
userInfo.setIdCardNum("idCardNum");
userInfo.setCardType("cardType");
preContractReq.setUserInfo(userInfo);
preContractReq.setContractId("contractId");when(mockContractService.preContract(preContractReq)).thenReturn(res);// Run the testfinalMockHttpServletResponse response = mockMvc.perform(post("/douyinPay/contract/preContract").content(JsonUtils.toJSONStringNotNull(preContractReq)).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andReturn().getResponse();// Verify the resultsassertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());assertThat(response.getContentAsString(StandardCharsets.UTF_8)).isEqualTo("{\"resCode\":\"9999\",\"resMessage\":\"服务异常\",\"resData\":null}");}
基于Dubbo接口的单元测试
使用dubbo泛化调用,具体讲解后续说明
publicclassDubboServiceTest{privatestaticfinalStringZK_ADDR="zookeeper://172.17.10.157:2181";privatestaticfinalStringAPP_NAME="test";privatestaticfinalStringVERSION="1.0.0";@TestpublicvoiddubboTest(){Class interfaceName =MsgServiceMultiInterface.class;//获取指定类型的dubbo接口MsgServiceMultiInterface context =getInterfaceByClassType(interfaceName);ModelsMessageRequest modelsMessageRequest =newModelsMessageRequest();ModelsReturn modelsReturn = context.message(modelsMessageRequest);}privatestaticMsgServiceMultiInterfacegetInterfaceByClassType(Class interfaceName){ApplicationConfig application =newApplicationConfig();
application.setName(APP_NAME);// 连接注册中心配置RegistryConfig registry =newRegistryConfig();
registry.setAddress(ZK_ADDR);// 注意:ReferenceConfig为重对象,内部封装了与注册中心的连接,以及与服务提供方的连接// 引用远程服务ReferenceConfig<MsgServiceMultiInterface> reference =newReferenceConfig<MsgServiceMultiInterface>();// 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
reference.setApplication(application);
reference.setRegistry(registry);// 多个注册中心可以用setRegistries()
reference.setInterface(interfaceName);
reference.setVersion(VERSION);MsgServiceMultiInterface context = reference.get();return context;}publicinterfaceMsgServiceMultiInterface{ModelsReturnmessage(ModelsMessageRequest var1);}
mockito
mock最大的功能是帮你把单元测试的耦合分解开,如果你的代码有一个类、接口的依赖,它能够帮你模拟这些依赖并帮你验证所调用的依赖的一些行为。
假设代码中有如下图所示的依赖:
当我们需要测试A类的时候,如果没有mock那么我们需要将整个依赖构建出来,而mock则可以将结构分解开,如下图所示:
使用范畴
- 真实对象具有不可确定的行为,不可预测
- 真实对象不好创建
- 真是对象的某些行为很难触发
- 真实对象实际上还不存在(比如该接口是三方或者其他开发小组的某个接口)
基本API介绍
方法名描述
mock()
/
@Mock
Mock是指使用Mockito创建的模拟对象,它模拟真实对象的行为,用于替代真实对象的依赖项,以便进行独立的单元测试。
@InjectMocks
@InjectMocks是一个Mockito注解,用于自动将模拟对象注入到被测对象中的相应字段中
doReturn()
doReturn()方法用于为模拟对象设置方法调用的返回值,可以覆盖默认行为。
when()
when()方法用于指定模拟对象的方法调用,并设置相应的操作,例如返回值、异常等。
verify()
verify()方法用于验证模拟对象的方法是否被调用,并可以进一步验证方法的调用次数和参数。
这里就不一一叙述了,可以参照:
API介绍:https://www.jianshu.com/p/54a28f6adf70
结合项目使用:https://zhuanlan.zhihu.com/p/640890853
squareTest插件使用
squareTest是一款自动生成单元测试的插件,覆盖率较广,比较推荐使用,可以减少测试工作量。
前置检查
- 引入mockito相应依赖
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.23.4</version> <scope>test</scope> </dependency>
- 检查是否有测试目录,若没有则添加,否则可能生成的时候出现如下弹窗
使用介绍
- 官网地址:https://squaretest.com/,其中也有使用视频教程
- 官方用户手册:https://squaretest.com/#user_guide 在插件市场搜索下载即可,squareTest提供30天的试用 可自行选择需要mock的依赖和方法
异常情况解决
- 若在生成单元测试代码时出现如下图弹框,需自定义设定测试路径
- 若代码运行单元测试时还有verify、或assert相关的错误找到对应错误位置直接删除即可,也可以验证的时候替换为Assert.assertNotNull(返回结果),最终是不会影响代码覆盖率的
- 生成代码是对于Controller层的代码无法精确到业务,需自行调整找到对应的Controller调用对应的service进行生成,所以对于Controller层的建议分别对service和Controller进行测试代码生成
- 对于没有覆盖测试到的地方,需要根据判断的字段进行针对性的赋值
jacoco测试覆盖插件使用
依赖引入
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.0.0-M5</version><configuration><testFailureIgnore>true</testFailureIgnore><includes><!--包含Test后缀的文件--><include>*/*Test.java</include></includes><excludes><!--<exclude>*/*Test.java</exclude>--></excludes><!-- 解决Jacoco错误,错误信息: Skipping JaCoCo execution... --><!--<argLine>${argLine} -Xmx2048m</argLine>--></configuration></plugin><plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.7</version><executions><execution><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>test</phase><goals><goal>report</goal></goals><configuration><!--定义输出的文件夹--><outputDirectory>${project.build.directory}/jacoco-report</outputDirectory></configuration></execution></executions></plugin>
使用介绍
导入依赖后使用maven test,插件会在target生成对应index.html,打开即可看到对应测试情况
注意:
单元测试的结构目录必须与源码的工程目录相同!!!否则会出现检测不到对应测试类
On the fly动态覆盖率模式(待完善)
Idea coverage使用
对于coverage没有达到预期效果,则可以idea自带代码覆盖工具,该工具可以配置第三方(例如:jacoco)并可以以html形式报告呈现,下面讲解下Idea自带覆盖率扫描工具的使用。
- 基于包级别的coverage1. 找到需要测试的单元测试类2. 运行后右侧会有coverage边框,会扫描测试类对应包的所有类,并显示对应的覆盖率3. 通过代码覆盖情况可修改对应参数,提高覆盖率(红色-未覆盖到 ,绿色-已覆盖)4. 也可以通过直接找到项目对应的包进行run converage
EasyRandom使用
在测试类中构造对象时使代码比较冗余,使用EasyRandom可高效构造对象
依赖引入
<dependency><groupId>org.jeasy</groupId><artifactId>easy-random-core</artifactId><version>4.3.0</version><exclusions><!-- 跟 SpringAOP 引入的 objenesis 有冲突,要排除 --><exclusion><groupId>org.objenesis</groupId><artifactId>objenesis</artifactId></exclusion></exclusions></dependency><!-- 支持根据参数校验逻辑生成对象字段 --><dependency><groupId>org.jeasy</groupId><artifactId>easy-random-bean-validation</artifactId><version>4.3.0</version></dependency>
封装工具类
importorg.jeasy.random.EasyRandom;importorg.jeasy.random.EasyRandomParameters;importorg.jeasy.random.api.Randomizer;importorg.jeasy.random.api.RandomizerRegistry;importjava.lang.reflect.Field;importjava.math.BigDecimal;importjava.util.List;importjava.util.Set;importjava.util.stream.*;importstaticorg.apache.commons.lang3.RandomUtils.nextDouble;/**
* 随机工具,封装 EasyRandom 提供的对象和 List 的随机生成方法
*
* @author lilei
*/publicclassRandomUtil{privatestaticfinalEasyRandom easyRandom;static{EasyRandomParameters param =newEasyRandomParameters();
param.setStringLengthRange(newEasyRandomParameters.Range<>(5,10));// 注册自定义随机生成器
param.randomizerRegistry(newBigDecimalRegistry());// 生成的对象是一个是接口或者抽象类,则扫描类路径找到它的一个具体实现类
param.setScanClasspathForConcreteTypes(true);
param.setObjectPoolSize(100);
param.setRandomizationDepth(12);
easyRandom =newEasyRandom(param);}privateRandomUtil(){thrownewUnsupportedOperationException("静态工具类不允许被实例化");}/**
* 根据给定的类型生成一个随机的对象
*/publicstatic<T>TnextObject(Class<T> clz){return easyRandom.nextObject(clz);}/**
* 根据给定的类型和大小生成一个随机对象的列表
*/publicstatic<T>List<T>nextList(Class<T> clz,int size){returnobjects(clz, size).collect(Collectors.toList());}/**
* 根据给定的类型和大小生成一个随机对象的集合
*/publicstatic<T>Set<T>nextSet(Class<T> clz,int size){returnobjects(clz, size).collect(Collectors.toSet());}/**
* 根据给定的类型和大小生成一个随机对象流
*/publicstatic<T>Stream<T>objects(Class<T> clz,int size){return easyRandom.objects(clz, size);}/**
* 默认生成的 BigDecimal 实例精度过大,将会导致用于插入数据库时超过精度而报错,所以我们默认精度取为5
*/privatestaticclassBigDecimalRegistryimplementsRandomizerRegistry{staticfinalBigDecimalRandomizer bigDecimalRandomizer =newBigDecimalRandomizer();@Overridepublicvoidinit(EasyRandomParameters easyRandomParameters){}@OverridepublicRandomizer<?>getRandomizer(Field field){if(field.getType()==BigDecimal.class){return bigDecimalRandomizer;}else{returnnull;}}@OverridepublicRandomizer<?>getRandomizer(Class<?> aClass){if(aClass ==BigDecimal.class){return bigDecimalRandomizer;}else{returnnull;}}}privatestaticclassBigDecimalRandomizerimplementsRandomizer<BigDecimal>{@OverridepublicBigDecimalgetRandomValue(){returnBigDecimal.valueOf(nextDouble(Integer.MIN_VALUE,Integer.MAX_VALUE)).setScale(5,BigDecimal.ROUND_HALF_UP);}}}
使用方式
// 注意:对于有泛型的对象无法直接进行初始化,需单独进行初始化,内部的对象再使用randomUtil// 随机生成一个 user 对象User user =RandomUtil.nextObjet(User.class);// 随机生成一个 user 对象列表List<User> users =RandomUtil.nextList(User.class,10);
版权归原作者 夜來风羽声 所有, 如有侵权,请联系我们删除。