单元测试(UnitTesting)是指对软件或项目中最小的可测试单元的正确性进行的测试工作。单元是人为指定的最小可测试功能模块,可以是模块、函数或类。单元测试需要独立于模块开发进行测试。程序开发完成后,我们往往不能保证程序100%正确。通过单元测试的编写,我们可以通过自动化测试程序定义我们的输入输出程序,通过断言检查每个Case的结果,测试我们的程序。为了提高程序的正确性、稳定性和可靠性,节省程序开发的时间。我们在项目中主要使用的单元测试框架有Spring-Boot-TestTestNG、PowerMock等,TestNG即TestingNextGeneration,下一代测试技术,是一套基于使用注解加强测试功能的JUnit和NUnit。它可用于单元测试或集成测试。PowerMock也是一个单元测试模拟框架,是在其他单元测试模拟框架的基础上做的扩展。PowerMock通过提供自定义类加载器和一些字节码篡改技术的应用,实现了对静态方法、构造方法、私有方法、Final方法的模拟支持,以及去除静态初始化过程等强大功能。常用注解1.TestNG注解@BeforeSuite仅在套件中所有测试运行在注解方法之前运行一次@AfterSuite仅在套件中所有测试运行在注解方法之后运行一次@BeforeClass调用当前类在第一次测试之前运行方法,注解的方法只运行一次@AfterreClass在调用当前类的第一个测试方法后运行,注解的方法只运行一次@BeforeMethod注解的方法将在每个测试方法之前运行@AfterMethod注解的方法将在每个测试方法之后运行@BeforeTest注解的方法将在测试标签内属于类的所有测试方法运行之前运行@AfterTest注释的方法将在测试标签内属于类的所有测试方法之后运行@DataProvider标签为测试方法提供数据的一种方式。带注释的方法必须返回一个Object[][],其中每个Object[]都可以分配给测试方法的参数列表。从该DataProvider接收数据的@Test方法需要使用与该注解名称相同的dataProvider名称。@Parameters描述了如何将参数传递给@Test方法;适用于xml方法的参数化方法通过值@Test将类或方法标记为测试的一部分,如果将??此标记放在类上,则该类的所有公共方法将被用作测试方法2。PowerMock注解@Mock注解其实就是Mockito.mock()方法的缩写,我们只在测试类中使用它;@InjectMocks主动将已有的mock对象注入到bean中,按名称注入,注入失败不会抛出异常;@Spy封装了一个真实的对象,使其可以像其他模拟对象一样被跟踪和设置行为;示例代码1.添加pom.xml依赖以Spring-Boot项目为例,首先需要添加TestNG+ProwerMock依赖如下:org.springframework.bootspring-启动启动器测试testorg.testngtestng${testng.version}测试org.powermockpowermock-api-mockito2${powermock.version}testorg.powermockpowermock-module-junit4${powermock.version}的版本>测试org.powermockpowermock-module-testng${powermock.version}版本测试2.增加元测试增加测试代码importorg.testng.annotations.Test;importstaticorg.junit.jupiter.api.Assertions.*;importstaticorg.mockito.Mockito.when;publicclassOrderServiceTestextendsPowerMockTestCase{@BeforeMethodpublicvoidbefore(){MockitoAnnotations.openMocks(this);}@InjectMocksprivateOrderServiceorderService;@MockprivateUserServiceuserService;//正常测试@TestpublicvoidtestCreateOrder(){//1.mockmethodstartUserDtouserDto=newUserDto();userDto.setId(100);when(userService.get()).thenReturn(userDto);//2.callbusinessmethodOrderDtoorder=orderService.createOrder(newOrderDto());//3.assertassertEquals(order.getId(),100);}//异常测试@TestpublicvoidtestCreateOrderEx(){//1.mockmethodstartwhen(userService.get()).thenThrow(newRuntimeException());Exceptionexception=null;try{//2.callbusinessmethodorderService.createOrder(newOrderDto());}catch(RuntimeExceptione){exception=e;}//3.assertassertNotNull(exception);}}普通Mock方法1.Mock静态方法//静态方法UserDtodto=newUserDto();dto.setId(100000);PowerMockito.mockStatic(UserService.class);PowerMockito.when(UserService.loginStatic()).thenReturn(dto);UserDtouserDto=UserService.loginStatic();assertEquals(100000,userDto.getId().intValue());2。模拟私有属性//字段赋值ReflectionTestUtils.setField(orderService,"rateLimit",99);3.Mock私有方法//模拟私有方法MemberModifier.stub(MemberMatcher.method(UserService.class,"get1")).toReturn(newUserDto());//测试私有方法Methodmethod=PowerMockito.method(UserService.class,"get1"",Integer.class);ObjectuserDto=method.invoke(userService,1);assertTrue(userDtoinstanceofUserDto);高级使用1.参数化批量测试当测试数据很多的时候,我们可以通过@DataProvider生成一个数据源,通过@Test(dataProvider="xxx")使用数据如下:importcom.test.testng.BaseTest;importcom.test.testng.dto.UserDto;importorg.mockito.InjectMocks;importorg.testng.annotations.DataProvider;importorg。testng.annotations.Test;importstaticorg.testng.Assert.assertFalse;importstaticorg.testng.AssertJUnit.assertTrue;publicclassUserServiceTest2extendsBaseTest{@InjectMocksprivateUserServiceuserService;//定义数据源@DataProvider(name="test")publicstaticObject[][]userList(){UserDtodto1=newUserDto();UserDtodto2=newUserDto();dto2.setSex(1);UserDtodto3=newUserDto();dto3.setSex(1);dto3.setFlag(1);UserDtodto4=newUserDto();dto4.setSex(1);dto4.setFlag(1);dto4.setAge(1);returnnewObject[][]{{dto1,null},{dto2,null},{dto3,null},{dto4,null}};}//正确场景@TestpublicvoidtestCheckEffectiveUser(){UserDtodto=newUserDto();dto.setSex(1);dto.setFlag(1);dto.setAge(18);booleanresult=userService.checkEffectiveUser(dto);assertTrue(result);}//错误场景@Test(dataProvider="test")publicvoidtestCheckEffectiveUser(UserDtodto,Objectobject){booleanresult=userService.checkEffectiveUser(dto);assertFalse(result);}}2.复杂判断确保测试覆盖案例:1.判断有效用户:年龄大于18且sex=1且flag=1publicbooleancheckEffectiveUser(UserDtodto){//判断有效用户:年龄大于18且sex=1且flag=1returnObjects.equals(dto.getSex(),1)&&Objects.equals(dto.getFlag(),1)&&dto.getAge()!=null&&dto.getAge()>=18;}2.将逻辑拆分为最简单的if...其他声明。然后添加单元测试如下:publicbooleancheckEffectiveUser(UserDtodto){if(!Objects.equals(dto.getSex(),1)){returnfalse;}if(!Objects.equals(dto.getFlag(),1)){returnfalse;}if(dto.getAge()==null){returnfalse;}if(dto.getAge()<18){returnfalse;}returntrue;}3.拆分后,我们可以看到我们只需要5个单元测试就可以实现全覆盖。publicclassUserServiceTestextendsBaseTest{@InjectMocksprivateUserServiceuserService;//覆盖第二个return@TestpublicvoidtestCheckEffectiveUser_0(){UserDtodto=newUserDto();booleanresult=userService.checkEffectiveUser(dto);assertFalse(result);}//覆盖第二个return@TestpublicvoidtestCheckEffectiveUser_0(){UserDtodto=newUserDto();booleanresult=userService.checkEffectiveUser(dto);assertFalse(result);}//覆盖第二个return@TestUseridtest{CheckEffective_1()UserDtodto=newUserDto();dto.setSex(1);booleanresult=userService.checkEffectiveUser(dto);assertFalse(result);}//覆盖第三个return@TestpublicvoidtestCheckEffectiveUser_2(){UserDtodto=newUserDto();dto.setSex(1);dto.setFlag(1);booleanresult=userService.checkEffectiveUser(dto);assertFalse(result);}//覆盖第四个return@TestpublicvoidtestCheckEffectiveUser_3(){UserDtodto=newUserDto();dto.setSex(1);dto.setFlag(1);dto.setAge(1);booleanresult=userService.checkEffectiveUser(dto);assertFalse(result);}//覆盖第5个return@TestpublicvoidtestCheckEffectiveUser_4(){UserDtodto=newUserDto();dto.setSex(1);dto.setFlag(1);dto.setAge(18);booleanresult=userService.checkEffectiveUser(dto);assertTrue(结果);}}4.单测覆盖检测检测3.通过断言验证方法参数1.assert:Assertion是java中的保留字,用来调试程序,后面接逻辑运算表达式,如下:inta=0,b=1;asserta==0&&b==0;//使用方法:javac编译源文件,然后java-eaclass文件名即可2.Spring-Boot中,提供的Assert类的方法通过Spring可用于前端来验证参数,如://Checkage>=18yearsoldpublicbooleancheckUserAge(UserDtodto){Assert.notNull(dto.getAge(),"Useragecannotbeempty");Assert.isTrue(dto.getAge()>=18,"用户年龄不能小于18");returnBoolean.TRUE;}3.如果需要转换为restapi返回的统一对应消息,我们可以通过:@ControllerAdvicepublicclassGlobalExceptionHandler{@ResponseBody@ExceptionHandler(value=IllegalArgumentException.class)publicResponsehandleArgError(IllegalArgumentExceptione){returnnewResponse().failure().message(e.getMessage());}}总结原则上,我们在功能模块的设计过程中应该遵循的原则(参考《软件工程-结构化设计准则》):模块大小适中,系统调用深度多扇入少扇出(提高复用度,降低依赖度)。单入口和单出口模块的范围在模块内应该是可预测的。内聚、低耦合的系统分解具有较少的数据冗余级别参考文档https://testng.org/doc/https://github.com/powermock/powermockhttps://www.netconcepts.cn/detail-41004.html