文章目录
前言
本篇文章旨在介绍单元测试的基本概念,介绍如何使用Mockito+PowerMock进行单元测试,对开发中坏代码进行预检测。在实际开发中,如果我们想保证代码的高质量,那必然需要写大量单元测试,需要对代码的各类各行及各分支覆盖的非常全面,而伴随而来的是我们又会面临代码在提交合并跑流水线的时间过长等问题,因此后续的文章将介绍一些涉及到单元测试的效率问题和其优化方法。
一、单元测试是什么?
单元测试(unit testing)是指对软件中的最小可测试单元进行检查和验证。它是软件测试中的一种基本方法,也是软件开发过程中的一个重要步骤。单元测试的目的是在于确保软件的每个独立模块都被正确地测试,并且没有潜在的缺陷或漏洞。在单元测试中,需要对每个模块进行测试,以确保它们能够按照预期的方式工作,并且没有任何错误或漏洞。
单元测试通常包括以下几个步骤:
确定测试范围:在开始测试之前,需要确定测试的范围,即要测试的功能或模块:函数、方法或类。
确定测试目标:确定要测试的功能或行为,并编写测试用例。
编写测试代码:根据确定的测试范围和测试目标,编写测试代码和用例,测试其功能或行为,这些代码和用例应该覆盖软件中的每个模块。
执行测试用例:使用测试工具(如JUnit、TestNG、Mock等)执行测试用例,以确保每个模块都按照预期的方式工作。
分析测试结果:在测试完成后,需要分析测试结果,以确定是否存在缺陷或漏洞。
修复缺陷或漏洞:如果发现缺陷或漏洞,需要修复它们,以确保软件的质量。
二、单元测试意义
提高代码质量和可靠性:单元测试可以检测和修复代码中的错误和缺陷,从而提高代码的质量和可靠性。
支持代码重构:单元测试可以在代码重构时提供保障,避免代码重构后引入新的错误和缺陷。
减少调试时间:单元测试可以快速定位和修复代码中的错误,从而减少调试时间和成本。
改善代码设计:单元测试要求程序员编写可测试的代码,这可以促进代码的模块化和松耦合,从而改善代码的设计和可维护性。
提高开发效率:单元测试可以自动化执行,从而节省开发人员的时间和精力,提高开发效率。
三、jar包引入
注意:本篇文章介绍的是Mockito框架和PowerMock框架的使用
Mockito 是一个流行的 Java 单元测试框架,用于模拟(mock)对象以便进行单元测试。它可以帮助开发人员创建和管理模拟对象,以便在测试过程中替换那些不容易构造或获取的对象,从而达到测试被测代码的行为,而无需依赖于实际的外部系统或服务。
Mockito的主要特点包括:
模拟对象:Mockito 允许创建模拟对象,这些模拟对象具有与真实对象相似的行为,但实际上只是虚拟的对象实例。通过模拟对象,可以模拟外部依赖、交互行为等,从而使测试更加独立和可控。
验证行为:Mockito 提供了丰富的 API 来验证模拟对象的交互行为,例如方法是否被调用、调用次数、参数匹配等。通过验证行为,可以确保被测代码按预期执行了与外部系统的交互。
Stubbing 方法:Mockito 允许对模拟对象的方法进行 Stubbing,即定义当调用某个方法时应该返回的值。这样可以模拟不同的场景和条件,以覆盖多种测试情况。
灵活性:Mockito 提供了简洁且易于使用的 API,支持与 JUnit、TestNG 等测试框架集成。同时,它还提供了丰富的功能和扩展,可以满足各种测试场景的需求。
PowerMock是一个强大的Java单元测试框架,它扩展了JUnit和TestNG的功能,允许开发者模拟静态方法、私有方法、构造函数以及最终类。PowerMock的出现,解决了传统单元测试工具无法覆盖的测试场景,使得测试更加全面和深入。
PowerMock的主要特点包括:
模拟静态方法:PowerMock能够模拟Java中的静态方法,这是传统测试框架所无法做到的。
模拟私有方法:通过PowerMock,开发者可以轻松测试类的私有方法。
模拟构造函数:PowerMock允许模拟对象的构造过程,这对于测试依赖于特定构造行为的代码非常有用。
模拟最终类:PowerMock提供了模拟Java最终类(final classes)的能力,打破了Java语言层面的限制。
高级模拟特性:包括模拟回调、结果和序列等高级功能,使得测试更加灵活和强大。
1. 各jar包版本依赖关系
方式一:目前来说SpringBoot默认的Mock框架是Mockito,和junit一样,只需要依赖spring-boot-starter-test就可以。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
方式二:如果是单独添加依赖,那么可以按照如下添加依赖
**推荐此方式**
<properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><powermock.version>2.0.7</powermock.version><mockito.core.version>2.13.0</mockito.core.version></properties><dependencies><!-- powermock jar引入 --><dependency><groupId>org.powermock</groupId><artifactId>powermock-api-mockito2</artifactId><version>${powermock.version}</version><scope>provided</scope></dependency><dependency><groupId>org.powermock</groupId><artifactId>powermock-api-support</artifactId><version>${powermock.version}</version></dependency><dependency><groupId>org.powermock</groupId><artifactId>powermock-module-junit4</artifactId><version>${powermock.version}</version><exclusions><exclusion><groupId>org.objenesis</groupId><artifactId>objenesis</artifactId></exclusion></exclusions></dependency><!-- mockito jar引入 --><dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>${mockito.core.version}</version><exclusions><exclusion><groupId>org.objenesis</groupId><artifactId>objenesis</artifactId></exclusion></exclusions><scope>provided</scope></dependency></dependencies>
注意:Mockito和PowerMock框架各版本互相支持的关系需要特别注意,不然运行代码时会遇到各种问题,可以直接参考本文的版本管理复制即可。
powermock-api-mockito 支持 mockito 1.x
powermock-api-mockito2 支持 mockito 2.x
2. 各jar包核心类的介绍
相信大家对于上述jar包版本的选择提高了关注度,且CV大法可以直接让大家避免出现各种版本冲突,代码运行不起来的问题。emm…版本问题确实是解决了,但不知道大家是否有这样的疑惑:写一个单元测试需要引入这么多jar包?这每个jar包到底有什么用啊?带着这样的问题,我们就能更好的写单测,充分利用各个jar包提供的功能,其每个jar包中提供的特别的类,提供的特别的能力将在下文实战开发小节里充分体现。咱们先来说说各个jar包提供了哪些特别的类和特别的能力。
<dependency><groupId>org.powermock</groupId><artifactId>powermock-api-mockito2</artifactId><version>${powermock.version}</version></dependency>
上面jar包提供了PowerMockito类和Slf4jMockPolicy类等,其主要作用是mock静态方法、私有方法等,提供一些Mockito框架无法提供的功能,其类中各方法的如何使用将在实战开发小节详细介绍。
<dependency><groupId>org.powermock</groupId><artifactId>powermock-api-support</artifactId><version>${powermock.version}</version></dependency>
上面jar包提供了MemberModifier类和MemberMatcher类等,其主要作用是mock本类里的其他私有方法的调用,其类中各方法的如何使用将在实战开发小节详细介绍。
<dependency><groupId>org.powermock</groupId><artifactId>powermock-module-junit4</artifactId><version>${powermock.version}</version><exclusions><exclusion><groupId>org.objenesis</groupId><artifactId>objenesis</artifactId></exclusion></exclusions></dependency>
上面jar包提供了PowerMockRunner类等,其主要作用是运行测试用例,提供环境可以让PowerMockito类mock静态方法和私有方法生效。其位置固定,伴随着@RunWith注解,写在每个类的开头。
<dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>${mockito.core.version}</version><exclusions><exclusion><groupId>org.objenesis</groupId><artifactId>objenesis</artifactId></exclusion></exclusions></dependency>
上面jar包提供了Mockito类和@InjectMocks注解和@Mock注解等,其主要作用是mock方法和变量等,它是mock框架的基本jar包,PowerMockito框架是在其基础上的延申和拓展,其类中各方法的如何使用将在实战开发小节详细介绍。
四、开发与实战
在本节开发与实战阶段中,文中会介绍大量的注解、大量的方法,当然每一个类提供的方法肯定都有它的目的性,是为具体的需求所服务的,大家按照自己的业务需要使用即可。本节将先介绍其基本概念,让大家对类中方法有个初步印象后,再按照前文第一节中介绍里的写单元测试的步骤去完整写一个单测,让大家真正的找到编写单元测试的感觉。
1. 基础知识入门
由于注解相比需要介绍的方法来说少的多,接下来先介绍注解这一块儿的知识点。
1.1 Mock注解:
@Runwith : 这个注解就是指定一个运行器,其可以与JUnit4.class、SpringJUnit4ClassRunner.class、SpringRunner.class、PowerMockRunner.class等搭配使用。
@PrepareForTest :注解用于指定需要被模拟的类。这些类中的静态方法、私有方法等都可以在测试中被模拟。
@PrepareForTest注解和@RunWith注解是结合使用的,不要单独使用它们中的任何一个,否则不起作用。事实上,在写一般的mock时,不需要使用这两个注解,只有当需要使用PowerMock去mock静态、final或者私有方法时,需要加上这两个注解,并且@RunWith需要搭配PowerMockRunner.class使用,而@PrepareForTest需要搭配被mock静态、final或者私有方法所在的类。 额外值得一提的是:在你输入@RunWith注解时,如果使用Eclipse开发,其可能会自动导入org.powermock.modules.junit4.legacy.PowerMockRunner包,记得把它换成org.powermock.modules.junit4.PowerMockRunner,否则会抛java.lang.NoClassDefFoundError.
@InjectMocks :Mockito会将@Mock或@Spy修饰的属性自动注入到@InjectMocks修饰的属性中。通常用来生成被测对象。
@Mock :为某个类创建一个虚假的对象,在测试时用这个虚假的对象替换掉真实的对象。通常用来生成被注入到被测对象中的对象
@Spy :当一个对象中的部分方法需要mock,部分方法需要真实调用的时候,我们可以使用该注解来解决这个问题。
mock框架的注解相关概念已经基本介绍完了,接下来具体介绍一些mock方法。然后,为了让大家在给自己的业务代码写单测时快速找到自己所需要的一些答案,这里先介绍这些mock框架方法的作用,换句话说是写业务代码时我们通常会面临的问题,然后再介绍哪些类或方法能起到这个作用或帮助我们解决这些问题。
1.2 Mock一般方法:
PowerMockito.when(clazzA.methodA(Mockito.anyObject())).thenReturn(xxx);PowerMockito.doThrow(newXxxException("")).when(clazzA).methodA(Mockito.any(XXX.class));
1.3 Mock静态方法:
PowerMockito.mockStatic(MyTestUtils.class);PowerMockito.when(MyTestUtils.methodA()).thenReturn(xxx);
注:若MyUtils的方法为staticvoid ,则默认会mock掉。
1.4 Mock私有方法:
例:如果mock一个类TestBoss,其方法methodA无入参,其方法methodB有入参param1,为int类型,param2,为List类型,那么可以按如下方式去mock
// 使用PowerMockito类去mockPowerMockito.when(TestBoss.class,"methodA").thenReturn(xxx);PowerMockito.when(TestBoss.class,"methodB", param1, param2).thenReturn(xxx);
或者PowerMockito.when(TestBoss.class,"methodB",Mockito.anyInt(),Mockito.anyList()).thenReturn(xxx);// 使用MemberModifier类去mockMemberModifier.stub(MemberMatcher.method(TestBoss.class,"methodB",int.class,List.class)).toReturn(xxx);
注:Mockito.anyInt()与int类型对应,Mockito.anyList()与List类型对应,Mockito.anyBoolean()与boolean类型对应......
代码中xxx的类型是我们所mock的方法的返回值类型。
如果是模拟异常情况,把thenReturn()替换成thenThrow(),把toReturn()替换成toThrow()即可
1.5 Mock构造方法:
说明:当我们需要mock方法内new出来的变量时,我们可以通过mock构造方法来实现。
// mock Person对象PowerMockito.whenNew(Person.class).withArguments(Mockito.anyString()).thenReturn(newPerson("HuGe"));// mock Person对象并且打桩Person mockPerson =Mockito.mock(Person.class);PowerMockito.whenNew(Person.class).withAnyArguments().thenReturn(mockPerson);PowerMockito.when(mockPerson.canAct()).thenReturn(true);// PowerMockito.when(mockPerson.canExecute()).thenThrow(new RuntimeException());
1.6 Mock抽象方法:
说明:我们一般选择去mock抽象类的实现类,而不是直接mock抽象类。
1.7 public方法的测试执行:
// 如果使用@InjectMocks注解为Person类生成了一个被测对象underTest,则其测试方法执行按下进行调用
underTest.method1(xx, xx);
1.8 private方法的测试执行:
// 如果使用@InjectMocks注解为Person类生成了一个被测对象underTest,则其测试方法执行按下进行调用Whitebox.invokeMethod(underTest,"method1", xx, xx);
或 WhiteboxImpl.invokeMethod(underTest,"method1", xx, xx);
1.9 有返回值方法断言:
有返回值类型断言[目的是断言方法的返回值与期望的返回值相同]// 若是方法返回值是boolean类型Assert.assertTrue()或者Assert.assertFalse()// 若是方法返回值是字符串类型、浮点数类型、整型类型......Assert.assertEquals(result, expectResult);// 若是方法返回值是List类型Assert.assertEquals(n, result.size())或者Assert.assertTrue(result.contains(xxx))或者Assert.assertTrue(result.isEmpty())// 若是方法返回值是普通对象类型
这种情况下,我们又可以回到前面几种类型,具体到这个对象的某个字段类型去做断言
// 若是方法返回值是nullAssert.assertNull(result)
1.10 无返回值方法断言:
无返回值类型断言[目的是断言方法的返回值与期望的返回值相同]// 方法是void方法,但是传入的是对象类型,改变了对象类型中的值
这种情况下我们可以断言其对象类型中的值
// 方法是void方法,且也没有传入对象类型,或者说传入了对象类型,但其值没有发生改变
这种情况下我们只能断言这个方法里调用其他的普通方法的执行次数
Mockito.verify(aaaRepository,Mockito.times(n)).method1();// 方法是void方法,且也没有传入对象类型,或者说传入了对象类型,但其值没有发生改变,且方法里只调用了静态方法
这种情况下我们只能断言这个方法里调用其他的静态方法的执行次数
使用PowerMockito检查某个静态方法调用的次数:
步骤:先调用对应的静态方法,再启用静态检查,最后再调用对应的静态方法。这样的用法比较奇怪,记住即可。
举例:验证Runtime.getRuntime()是否被调用了2次。
PowerMockito.mockStatic(Runtime.class);Runtime.getRuntime();Runtime.getRuntime();// do other thingsPowerMockito.verifyStatic(Mockito.times(2));Runtime.getRuntime();
1.11 小结
Mock注解其实是对于Mock代码的一种简化,在开发中,我们通常用@InjectMocks注解和@Mock注解去替代PowerMock.mock(XX.class),
用@Spy去替代PowerMock.mock(XX.class)。
还有,有认真阅读本小结的朋友会发现一个现象,本小节编写的顺序就是按照本文第一章节单元测试步骤的顺序编写的,大家可以带着这样的思路去给自己的业务代码编写单元测试,这样写起来就非常的丝滑。如果大家使用AI模型去编写单元测试,按照这个顺序去给模型给出的单元测试模板去进行调整,也会大大提高我们的调参效率。
2. 案例与实战
本节案例与实战中,将以gitee上的开源项目聚惠星商城项目为案例,然后带着大家按照第本文第一节单元测试步骤去写单元测试。以这个案例带大家找到实际写单元测试的感觉之后,以后大家就可以按照这个写单测的模式去给自己的业务代码编写单元测试,从而提高自己的代码质量,减少线上问题。
2.1 案例1
例如:针对项目中的CouponAssignService类生成单测。
@ServicepublicclassCouponAssignService{@AutowiredprivateDtsCouponUserService couponUserService;@AutowiredprivateDtsCouponService couponService;/**
* 分发注册优惠券
*
* @param userId
* @return
*/publicvoidassignForRegister(Integer userId){List<DtsCoupon> couponList = couponService.queryRegister();for(DtsCoupon coupon : couponList){Integer couponId = coupon.getId();Integer count = couponUserService.countUserAndCoupon(userId, couponId);if(count >0){continue;}Short limit = coupon.getLimit();while(limit >0){DtsCouponUser couponUser =newDtsCouponUser();
couponUser.setCouponId(couponId);
couponUser.setUserId(userId);Short timeType = coupon.getTimeType();if(timeType.equals(CouponConstant.TIME_TYPE_TIME)){
couponUser.setStartTime(coupon.getStartTime());
couponUser.setEndTime(coupon.getEndTime());}else{LocalDate now =LocalDate.now();
couponUser.setStartTime(now);
couponUser.setEndTime(now.plusDays(coupon.getDays()));}
couponUserService.add(couponUser);
limit--;}}}}
我们首先可以按照图中步骤去快速生成单测模板
接下来按步骤去写单元测试。
①确定测试范围
// CouponAssignService为待测试类,DtsCouponUserService和DtsCouponService是CouponAssignService所依赖的对象// assignForRegister为待测试方法@InjectMocksprivateCouponAssignService underTest;@MockprivateDtsCouponUserService couponUserService;@MockprivateDtsCouponService couponService;
②确定测试目标
// 因为assignForRegister是void方法,且没有传入对象改变其值, 即只能断言方法里的行为执行次数(其他方法执行次数)// couponUserService中的add方法执行次数
couponUserService.add()
③编写测试代码
// 因为需要让couponUserService执行add方法,即需要准备一些环境(用例),可以DtsCoupon dtsCoupon =newDtsCoupon();
dtsCoupon.setLimit((short)1);
dtsCoupon.setTimeType((short)1);PowerMockito.when(couponService.queryRegister()).thenReturn(Lists.newArrayList(dtsCoupon));PowerMockito.when(couponUserService.countUserAndCoupon(Mockito.anyInt(),Mockito.anyInt())).thenReturn(-1);
④执行测试用例
// 因为assignForRegister方法就是public方法
underTest.assignForRegister();
⑤分析测试结果
// 断言couponUserService.add方法执行次数Mockito.verify(couponUserService,Mockito.times(1)).add(Mockito.any());
汇总上述步骤
/**
* @author wasteland
* @create 2024-10-08
*/@RunWith(PowerMockRunner.class)@PrepareForTestpublicclassCouponAssignServiceTest{@InjectMocksprivateCouponAssignService underTest;@MockprivateDtsCouponUserService couponUserService;@MockprivateDtsCouponService couponService;@BeforepublicvoidsetUp()throwsException{}@TestpublicvoidassignForRegister(){// 测试环境准备DtsCoupon dtsCoupon =newDtsCoupon();
dtsCoupon.setLimit((short)1);
dtsCoupon.setTimeType((short)1);PowerMockito.when(couponService.queryRegister()).thenReturn(Lists.newArrayList(dtsCoupon));PowerMockito.when(couponUserService.countUserAndCoupon(Mockito.anyInt(),Mockito.anyInt())).thenReturn(-1);// 测试方法执行
underTest.assignForRegister(2);// 测试结果断言Mockito.verify(couponUserService,Mockito.times(1)).add(Mockito.any());}}
2.2 案例2
例如:针对项目中的DtsAccountService类生成单测(此案例略去如何生成单元测试模板,分步骤写,直接给出按步骤写的最终的单元测试类结果)
@ServicepublicclassDtsAccountService{privatestaticfinalLogger logger =LoggerFactory.getLogger(DtsAccountService.class);publicstaticfinallong TWO_MONTH_DAYS =60;//近两个月,60天publicstaticfinallong ONE_WEEK_DAYS =7;//近一周@ResourceprivateDtsUserAccountMapper userAccountMapper;@ResourceprivateDtsAccountTraceMapper accountTraceMapper;@ResourceprivateAccountMapperEx accountMapperEx;@ResourceprivateDtsUserMapper userMapper;publicDtsUserAccountfindShareUserAccountByUserId(Integer shareUserId){DtsUserAccountExample example =newDtsUserAccountExample();
example.or().andUserIdEqualTo(shareUserId);List<DtsUserAccount> accounts = userAccountMapper.selectByExample(example);// Assert.state(accounts.size() < 2, "同一个用户存在两个账户");if(accounts.size()==1){return accounts.get(0);}else{
logger.error("根据代理用户id:{},获取账号信息出错!!!",shareUserId);returnnull;}}publicList<Integer>findAllSharedUserId(){return accountMapperEx.getShareUserId();}privateStringgetRandomNum(Integer num){String base ="0123456789";Random random =newRandom();StringBuffer sb =newStringBuffer();for(int i =0; i < num; i++){int number = random.nextInt(base.length());
sb.append(base.charAt(number));}return sb.toString();}.....}
/**
* @author wasteland
* @create 2024-10-08
*/@RunWith(PowerMockRunner.class)@PrepareForTestpublicclassDtsAccountServiceTest{@InjectMocksprivateDtsAccountService underTest;@MockprivateDtsUserAccountMapper userAccountMapper;@BeforepublicvoidsetUp()throwsException{}@TestpublicvoidfindShareUserAccountByUserId_returnNull(){// 测试环境准备PowerMockito.when(userAccountMapper.selectByExample(Mockito.any())).thenReturn(Lists.newArrayList());// 测试方法执行DtsUserAccount result = underTest.findShareUserAccountByUserId(1);// 测试结果断言Assert.assertNull(result);}@TestpublicvoidfindShareUserAccountByUserId_returnNotNull(){// 测试环境准备DtsUserAccount dtsUserAccount1 =newDtsUserAccount();
dtsUserAccount1.setUserId(1);PowerMockito.when(userAccountMapper.selectByExample(Mockito.any())).thenReturn(Lists.newArrayList(dtsUserAccount1));// 测试方法执行DtsUserAccount result = underTest.findShareUserAccountByUserId(1);// 测试结果断言Assert.assertEquals(1, result.getUserId(),0);}@TestpublicvoidgetRandomNum()throwsException{// 测试环境准备[由于这个方法很简单,不需要额外的环境构建]// 测试方法执行String result =WhiteboxImpl.invokeMethod(underTest,"getRandomNum",1);// 测试结果断言Assert.assertEquals(1, result.length());}}
五、小技巧
1. 单元测试覆盖率查看工具:
以下两种方式都可以查看我们对代码中类、方法、行的单测覆盖率。
1.1 使用IDEA自带的代码覆盖率工具
1.2 使用IDEA集成Jacoco(IDEA版本可能会有不同)
1.pom文件增加依赖,然后运行单测收集覆盖率
<dependency><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.2</version></dependency><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin><plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.2</version><configuration><destFile>target/coverage-reports/jacoco-unit.exec</destFile><dataFile>target/coverage-reports/jacoco-unit.exec</dataFile></configuration><executions><execution><id>jacoco-initialize</id><goals><goal>prepare-agent</goal></goals></execution><!--这个report:对代码进行检测,然后生成index.html在 target/site/index.html中可以查看检测的详细结果--><execution><id>jacoco-site</id><phase>package</phase><!--<phase>test</phase>写上test的时候会自动出现site文件夹,而不需执行下面的jacoco:report步骤,推荐--><goals><goal>report</goal></goals></execution></executions></plugin></plugins></build>
2.之前需要在pom里显示引入jacoco,但现在idea已经集成了jacoco,可以在运行单测之前直接切换为jacoco运行,如下图所示:
2. 单元测试编写效率提升工具:
以下工具我使用过前两个,其可以一定程度上帮助我们自动生成单元测试类,提高开发效率,特别是对业务逻辑相对简单类编写单元测试,它们还是较为好用的。听说第3个工具最好用,如果大家有兴趣的话可以尝试一下!
2.1 传统插件:Squaretest
2.2 AI插件:通义灵码【阿里推出且免费】
2.3 AI插件:Copilot【OpenAI推出但收费】
创作不易,如果有帮助到你的话请给点个赞吧!我是Wasteland,下期文章再见!
版权归原作者 wasteland~ 所有, 如有侵权,请联系我们删除。