基于Springboot+Junit+Mockito做单元测试的示例
前言
这篇文章介绍如何使用Springboot+Junit+Mockito做单元测试,案例选取撮合交易的一个类来做单元测试。
单元测试前先理解需求
要写出好的单测,必须先理解了需求,只有知道做什么才能知道怎么测。但本文主要讲mockito的用法,无需关注具体需求。所以本节略去具体的需求描述。
隔离外部依赖
Case1.被测类中被@Autowired或@Resource注解标注的依赖对象,如何控制其返回值
以被测方法MatchingServiceImpl.java的matching(MatchingOrderbuyOrder,MatchingOrdersellOrder)为例
被测类MatchingServiceImpl
publicclassMatchingServiceImplimplementsMatchingService{
privatestaticfinalLoggerlog=LoggerFactory.getLogger(MatchingServiceImpl.class);
@Autowired
privateQuoteServicequoteService;
...
publicMatchingResultmatching(MatchingOrderbuyOrder,MatchingOrdersellOrder){
intcurrentPrice=quoteService.getCurrentPriceByProduct(buyOrder.getProductCode());
MatchingResultresult=newMatchingResult();
if(sellOrder!=null&&buyOrder!=null&&
sellOrder.getPrice()<=buyOrder.getPrice()){
...
}
}
matching方法中的quoteService.getCurrentPriceByProduct(buyOrder.getProductCode());要访问Redis获取当前报价,这里我们需要把外部依赖quoteServicemock掉,控制getCurrentPriceByProduct方法的返回值。使用mockito可以做到,具体如下:
测试类MatchingServiceImplTest
publicclassMatchingServiceImplTestextendsMockitoBasedTest{
/**
*被@Mock标注的对象会自动注入到被@InjectMocks标注的对象中
*/
@Mock
privateQuoteServicequoteService;
/**
*
*被测对象,用@InjectMocks标注,那些被@mock标注的对象就会自动注入其中。
*另一个注意点是这里的MatchingServiceImpl是直接new出来(Mockito1.9版本后不new也可以),而不是通过spring容器注入的。因为这里我不需要从spring容器中
*获得其他依赖,不需要database,redis,zookeeper,mq,啥都不依赖,所以直接new
*
*/
@InjectMocks
privateMatchingServiceImplmatchingService=newMatchingServiceImpl();
@Test
publicvoidtestMatching_SuccessWhenCurrentPriceBetweenBuyPriceAndSellPrice(){
MatchingOrderbuyOrder=newMatchingOrder();
buyOrder.setPrice(1000);
buyOrder.setCount(23);
MatchingOrdersellOrder=newMatchingOrder();
sellOrder.setPrice(800);
sellOrder.setCount(20);
//方法打桩(Methodstubbing)
//when(x).thenReturn(y):当指定方法被调用时返回指定值
Mockito.when(quoteService.getCurrentPriceByProduct(Mockito.anyString())).thenReturn(900);
MatchingResultresult=matchingService.matching(buyOrder,sellOrder);
org.junit.Assert.assertEquals(true,result.isSuccess());//断言撮合是否成功
org.junit.Assert.assertEquals(20,result.getTradeCount());//断言成交数量
org.junit.Assert.assertEquals(900,result.getTradePrice());//断言最新报价是否符合预期
}
Case2.被测函数A调用被测类其他函数B,怎么控制函数B的返回值?
比如,MatchingServiceImpl中有个函数startBuyProcess,它里面调用了该类中的其他函数,如getTopSellOrder,matching,如何控制这两个函数的返回值?
这里要解决的问题其实是怎么对一个类”部分mock”–被测类的被测方法(如startBuyProcess)要真实执行,而另一些方法(如getTopSellOrder)则是要打桩(不真正进去执行)。
被测类MatchingServiceImpl
protectedvoidstartBuyProcess(MatchingOrderbuyOrder,booleanwaitForMatching){
while(true){
//对手方最优价
MatchingOrdertopSellOrder=getTopSellOrder(buyOrder.getProductCode());
MatchingResultmatchingResult=matching(buyOrder,topSellOrder);
if(matchingResult.isSuccess()){
doMatchingSuccess(buyOrder,topSellOrder,matchingResult,MatchingType.BUY);
if(buyOrder.getCount()<=0){
break;
}
}else{
if(waitForMatching){
//加入待撮合队列
addToMatchingBuy(buyOrder);
}else{
//撤单
sendCancleMsg(buyOrder);
}
break;
}
}
}
利用Mockito.spy()可以做到“部分Mock”
测试类MatchingServiceImplTest.testStartBuyProcess_InCaseOfMatchingSuccess
/**
*
*测试StartBuyProcess方法在撮合成功后的处理是否符合预期,即测试startBuyProcess方法进入下面这个判断分支后的行为
*{@linkMatchingServiceImpl#startBuyProcess(MatchingOrder,boolean)}
*
*
*if(matchingResult.isSuccess()){
*
*doMatchingSuccess(buyOrder,topSellOrder,matchingResult,MatchingType.BUY);
*
*if(buyOrder.getCount()<=0){
*break;
*}
*}
*
*
*/
@Test
publicvoidtestStartBuyProcess_InCaseOfMatchingSuccess(){
MatchingOrderbuyOrder=newMatchingOrder();
buyOrder.setPrice(700);
buyOrder.setCount(23);
//用Mockito.spy()对matchingService进行部分打桩
matchingService=Mockito.spy(matchingService);
MatchingResultfirstMatchingResult=newMatchingResult();
firstMatchingResult.setSuccess(true);
firstMatchingResult.setTradeCount(20);
MatchingResultsecondMatchingResult=newMatchingResult();
secondMatchingResult.setSuccess(false);
//doReturn(x).when(obj).method()对方法打桩,打桩后,程序执行这些方法时将按照预期返回指定值,未被打桩的方法将真实执行
//两个doReturn表示第一次调用matchingService.matching时返回firstMatchingResult,第二次调用返回secondMatchingResult
//因为startBuyProcess里有个while循坏,可能会多次执行matching方法
Mockito.doReturn(firstMatchingResult).doReturn(secondMatchingResult).when(matchingService)
.matching(Mockito.any(MatchingOrder.class),Mockito.any(MatchingOrder.class));
MatchingOrdersellOrder=newMatchingOrder();
sellOrder.setPrice(600);
sellOrder.setCount(20);
//对getTopSellOrder方法打桩
Mockito.doReturn(sellOrder).when(matchingService).getTopSellOrder(Mockito.anyString());
//对外部依赖jedis的方法进行打桩
Mockito.when(jedisClient.incrBy(Mockito.anyString(),Mockito.anyLong())).thenReturn(0L);
//startBuyProcess是被测函数,不打桩,会真实执行
matchingService.startBuyProcess(buyOrder,true);
//后面的校验和断言是测试doMatchingSuccess方法的行为的,这也是这个测试的目的
//verify可用来校验,某个类的方法被执行过多少次,这里是校验jedisClient.zremFirst是否被执行过1次
Mockito.verify(jedisClient,Mockito.times(1)).zremFirst(Mockito.anyString());
org.junit.Assert.assertEquals(3,buyOrder.getCount());
org.junit.Assert.assertEquals(0,sellOrder.getCount());
}
spy的用法已经演示完毕,下面从testStartBuyProcess_InCaseOfMatchingSuccess说下单元测试的“粒度”。
testStartBuyProcess_InCaseOfMatchingSuccess的目的是想测doMatchingSuccess,我们费了很大劲才把前面的一堆准备工作做完,才能去测doMatchingSuccess。
更好的实践应该是另起测试方法去单独测doMatchingSuccess,关注点也集中很多,doMatchingSuccess覆盖完了,再测startBuyProcess其实就只是覆盖下它本身的判断分支就行了。覆盖率照样达到,而且测试代码也更容易维护,testStartBuyProcess_InCaseOfMatchingSuccess由于考虑的职责太多,它很容易受到变化的影响,细小的东西改变,可能就会影响它的正常工作。
引入测试框架Maven依赖
junit junit 4.11 test org.mockito mockito-all 1.10.19 test org.springframework spring-test 4.2.5.RELEASE test
springboot+junit+mockito的上下文构建
MockitoBasedTest
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes=TestApplication.class)
publicabstractclassMockitoBasedTest{
@Before
publicvoidsetUp()throwsException{
//初始化测试用例类中由Mockito的注解标注的所有模拟对象
MockitoAnnotations.initMocks(this);
}
}
//其他测试类继承MockitoBasedTest
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。