1、 简介
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。UT一般针对某个实现功能的方法,用构造的入参和Mock来模拟实际运行的过程,然后通过断言等方式判断方法的返回值是否与预期相同,相同则视为通过UT测试。
UT测试是白盒测试的一种,需要尽量覆盖方法内的所有分支。方法中一般带有很多边界值的判断,正常的业务逻辑可能无法覆盖这些情况,难以判断极限情况下方法功能是否正确。因此UT测试应尽量提高代码覆盖率,如若所有分支的结果都符合预期,一般即可认为方法的功能是正确的。
在实际开发中,UT测试可集成在流水线上,每一次提交合并代码执行流水线时会把全部UT跑一遍。如若在本次开发中对逻辑的修改出现了问题,方法返回值出现了变化,该方法对应的UT就无法通过,所以UT可以一定程度上提前发现功能上的bug。
2、 一般用法与断言
首先UT目录一般会与源码目录保持一致,且命名上也与测试的类和方法一一对应,我们查看源码时可轻易找到类中方法对应的UT位置。
其次,UT需要引入对应的包,在Java中常用的是org.junit.Test。在UT的方法签名上必须加上@Test注解,否则无法识别,还可以选择添加@DisplayName用于描述场景详情。
最后是UT的编写。UT的目的是测试开发人员编写的功能方法,因此在UT中必须调用需要测试的方法。功能方法的入参将直接影响方法的执行效果,而UT的作用即是判断在该入参下功能方法的执行结果是否与预期结果一致,最常用的即是使用断言判断方法的返回值是否符合预期。导入包org.junit.Assert,Assert中包含多种断言判断方式,包括但不限于判断值是否相等的Assert.assertEqual,判断值是否为空或非空的Assert.assertNull与Assert.assertNotNull等。
// Apple.java
public class Apple {
private boolean isClean;
}
// AppleMgmtServie.java
public class AppleMgmtService{
public boolean eat(Apple apple){
if(apple.getIsClean()){
return true;
}else{
return false;
}
}
}
// AppleMgmtServiceTest.java
public class AppleMgmtServiceTest{
AppleMgmtService appleMgmtService;
@BeforeEach
void setUp() {}
@AfterEach
void after() {}
@Test
public void testEat(){
Apple appleClean = new Apple(true);
Apple appleNotClean = new Apple(false);
// run the test
boolean resultCLean = appleMgmtService.eat(appleClean);
boolean resultNotClean = appleMgmtService.eat(appleNotClean);
// assert
Assert.assertTrue(resultClean);
Assert.assertEquals(resultNotClean, false);
}
}
如上,我们构造了两个不同的入参,从而使testEat()方法覆盖到了eat()方法所有可能的分支。如果成功通过了UT则说明方法结果与设计一致,方法在功能上正确。
3、Mock预设复杂返回值
UT测试只测试功能不依赖于环境,所以只能测试类中的方法,我们的功能函数可能通过对象的方式调用了其中的方法,但当我们打断点调试UT时就会发现该对象为空,其中的方法自然无法执行。如,我们调用了一个外部对象WashMgmtService,其中有一个wash()方法:
// WashMgmtService.java
public class WashMgmtService{
public Apple wash(Apple apple){
apple.setIsClean(true);
return Apple;
}
}
eat()函数也做出了调整:
// AppleMgmtServie.java
public class AppleMgmtService{
@AutoWired
private WashMgmtService washMgmtService;
public boolean eat(Apple apple){
Apple appleClean = washMgmtService.wash(apple);
if(appleClean.getIsClean()){
return true;
}else{
return false;
}
}
}
随后运行UT测试,我们会发现方法抛出了NullPoninterException。随后打断点进行调试,原因是运行到调用wash()方法的那一行时,控制台显示washMgmtService是一个空对象。对于这种情况,UT无法获取到指定的对象,我们就需要使用Mock来模拟这个对象及该方法的返回值。mock本质上是一个proxy,在需要提供功能的时候由开发者提供“伪实现”,任何被非本类的功能均需要mock,如数据库访问、RPC接口、外部引入的jar包等。
washMgmtService是本次我们需要Mock的对象,因此在UT的类中也需要定义一个该类型的对象,并在其上添加注解@Mock。而我们UT测试的这个方法位于的对象上,则需添加对应的@InjectMocks,代表把Mock对象注入测试对象中,在测试对象的方法执行过程中需要Mock对象时能够取到。使用方式如下:
// AppleMgmtServiceTest.java
public class AppleMgmtServiceTest{
@Mock
WashMgmtService washMgmtService;
@InjectMock
AppleMgmtService appleMgmtService;
@BeforeEach
void setUp() {}
@AfterEach
void after() {}
@Test
public void testEat(){
Apple appleClean = new Apple(true);
Apple appleNotClean = new Apple(false);
// Mock
Apple appleReturn = new Apple(true);
Mockito.when(washMgmtService.wash(any())).doReturn(appleReturn);
//Mockito.doReturn(appleReturn).when(washMgmtService).wash(any());
// run the test
boolean resultCLean = appleMgmtService.eat(appleClean);
boolean resultNotClean = appleMgmtService.eat(appleNotClean);
// assert
Assert.assertTrue(resultClean);
Assert.assertEquals(resultNotClean, true);
}
}
其中参数any()也是Mock包中提供的,意义为无论传入wash的入参是何值都返回上面定义的appleReturn。需要注意的是Mock是对返回值的预设定,如若需要对该Mock的方法进行测试,则应该在该方法对应的UT中进行,本层不负责验证结果是否正确。
Mock只是一个模拟的过程,并不是构造出了一个对象并能够执行其中的方法,因此Mock方法中的返回值需要我们自定义。一般Mock方法使用的形式有when(对象x.方法y(参数z)).thenReturn(结果ret),也就是当测试方法执行到需要调用这个Mock对象x的方法y,且该Mock方法的参数为指定值z时,就返回结果ret。另外还有另一种常用形式, doReturn(结果ret).when(对象x).方法y(参数z),该方式与上述方式的差别是,第一种方式如果功能方法调用了同一个类的私有方法时则能直接寻找并执行,而第二种方式会把私有方法也同等视为需要模拟的对象方法,此处两种Mock的方式并无区别。