1)抛出异常的Mock
使用Assert.assertThrow或在UT方法签名的@Test注解上添加期望抛出的异常类型,当方法抛出该类型的异常时就会被UT捕获,并返回test成功。如若没抛出异常或抛出的异常与指定不符,该UT则无法通过。
// Apple.java
public class Apple {
private boolean isClean;
}
// AppleMgmtServie.java
public class AppleMgmtService{
public boolean eat(Apple apple){
if(apple.getIsClean()){
return true;
}else{
throw new MyException();
}
}
}
// AppleMgmtServiceTest.java
@Test(expected = MyException.class)
public class AppleMgmtServiceTest{
AppleMgmtService appleMgmtService;
@BeforeEach
void setUp() {}
@AfterEach
void after() {}
@Test
public void testEat(){
Apple appleNotClean = new Apple(false);
appleMgmtService.eat(appleNotClean);
// Assert.assertThrows(MyException, () -> appleMgmtService.eat(appleNotClean));
}
}
需要注意的是,这两种方式只能选择其一,因为异常只能被捕捉一次,如果使用了assertThrows对异常捕捉了后,expected注解就会捕捉不到异常而使UT无法通过。另外这种方法更适合校验自定义的异常,因为Java的标准异常可能在代码的任何一个地方出现,如NullPointerException,即使UT测试通过了,我们也无法判断这种异常出现的位置是否与预期相等。
2)无返回值或对入参对象进行修改的Mock
对于某些对象方法可能没有返回值,但是如果不Mock的话该对象在执行中还是为空,因此UT测试到这个位置仍会抛出NullPointerException异常。对此可以使用doNothing()的方式。其中还有一种特殊情况,虽然调用的方法没有返回值,但是却对入参的对象进行了修改,而我们测试的方法函数使用到了这个被修改的入参,如:
// WashMgmtService.java
public class WashMgmtService{
public void wash(Apple apple){
apple.setIsClean(true);
}
}
// AppleMgmtServie.java
public class AppleMgmtService{
@AutoWired
private WashMgmtService washMgmtService;
public boolean eat(Apple apple){
washMgmtService.wash(apple);
if(apple.getIsClean()){
return true;
}else{
return false;
}
}
}
这种情况就比较复杂,我们不仅需要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
Mockito.doNothing().when(washMgmtService)
.wash(argThat(new ArgumentMatcher<Apple>(){
@Override
public boolean matches(Apple apple){
apple.setIsClean(true);
return false;
}
}));
// run the test
boolean resultCLean = appleMgmtService.eat(appleClean);
boolean resultNotClean = appleMgmtService.eat(appleNotClean);
// assert
Assert.assertTrue(resultClean);
Assert.assertEquals(resultNotClean, true);
}
}
上述方法中,我们首先用了参数匹配器ArgumentMatcher固定wash方法的传入值是一个Apple类型的对象,随后在matches方法内模拟wash方法对这个入参的修改,在执行测试时就会执行整个模拟过程。所有对入参进行了修改,而功能方法又重新使用到这个入参对象的UT都需要使用这种方式模拟这个过程。
3)同类私有方法的Mock
对于本类函数中方法的Mock,虽然位于同一个类中的方法都可以被UT执行,但是测试功能方法调用的方法中可能比较复杂,我们不希望执行该方法而是直接用Mock来模拟返回值,如:
// AppleMgmtServie.java
public class AppleMgmtService{
@AutoWired
private WashMgmtService washMgmtService;
public boolean eat(Apple apple){
Apple appleClean = washNow(apple);
if(appleClean.getIsClean()){
return true;
}else{
return false;
}
}
private Apple washNow(Apple apple){
washMgmtService.wash(Apple);
return apple;
}
}
// WashMgmtService.java
public class WashMgmtService{
public void wash(Apple apple){
apple.setIsClean(true);
}
}
虽然测试eat方法时,编译器能顺利找到washNow方法的位置并执行,但washNow方法的执行过程我们并不需要关注,如上面这个例子在washNow中还调用了对象函数,比起模拟这个调用对象函数的过程我们还不如直接模拟washNow方法的结果。这种情况需要用到spy机制。Mock模拟的是功能类中调用的对象类,而spy模拟的即是功能类本身,可对其中的私有方法模拟返回值。
// AppleMgmtServiceTest.java
public class AppleMgmtServiceTest{
@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);
AppleMgmtService spyAppleMgmtService = Mockito.spy(new AppleMgmtService());
Mockito.doReturn(appleReturn).when(spyAppleMgmtService).washNow(any());
// run the test
boolean resultCLean = spyAppleMgmtService.eat(appleClean);
boolean resultNotClean = spyAppleMgmtService.eat(appleNotClean);
// assert
Assert.assertTrue(resultClean);
Assert.assertEquals(resultNotClean, true);
}
}
我们之前在测试eat方法是,用的都是普通的AppleMgmtService对象,在spy机制内我们则首先用spy方法构造一个受到监控的AppleMgmtService对象,在这之后的Mock和实际测试都要使用这个对象进行。在受到监控后,这个对象执行的方法也可以被Mock模拟,因此我们就可以构造私有方法的返回值。