前⾔
这篇⽂章介绍如何使⽤Springboot+Junit+Mockito做单元测试,案例选取撮合交易的⼀个类来做单元测试。单元测试前先理解需求
要写出好的单测,必须先理解了需求,只有知道做什么才能知道怎么测。但本⽂主要讲mockito的⽤法,⽆需关注具体需求。所以本节略去具体的需求描述。隔离外部依赖
Case1. 被测类中被@Autowired 或 @Resource 注解标注的依赖对象,如何控制其返回值
以被测⽅法 MatchingServiceImpl.java的matching(MatchingOrder buyOrder, MatchingOrder sellOrder)为例被测类MatchingServiceImpl
public class MatchingServiceImpl implements MatchingService {
private static final Logger log = LoggerFactory.getLogger(MatchingServiceImpl.class); @Autowired
private QuoteService quoteService; ...
public MatchingResult matching(MatchingOrder buyOrder, MatchingOrder sellOrder) { int currentPrice = quoteService.getCurrentPriceByProduct(buyOrder.getProductCode()); MatchingResult result = new MatchingResult(); if (sellOrder != null && buyOrder != null &&
sellOrder.getPrice() <= buyOrder.getPrice()) { ... }}
matching⽅法中的quoteService.getCurrentPriceByProduct(buyOrder.getProductCode());要访问Redis获取当前报价,这⾥我们需要把外部依赖quoteService mock掉,控制getCurrentPriceByProduct⽅法的返回值。使⽤mockito可以做到,具体如下:测试类MatchingServiceImplTest
public class MatchingServiceImplTest extends MockitoBasedTest { /**
* 被@Mock标注的对象会⾃动注⼊到被@InjectMocks标注的对象中 */
@Mock
private QuoteService quoteService; /**
*
*/* 被测对象,⽤@InjectMocks标注,那些被@mock标注的对象就会⾃动注⼊其中。
* 另⼀个注意点是这⾥的MatchingServiceImpl是直接new出来(Mockito 1.9版本后不new也可以),⽽不是通过spring容器注⼊的。因为这⾥我不需要从spring容器中 * 获得其他依赖,不需要database ,redis ,zookeeper,mq,啥都不依赖,所以直接new *
@InjectMocks
private MatchingServiceImpl matchingService = new MatchingServiceImpl(); @Test
public void testMatching_SuccessWhenCurrentPriceBetweenBuyPriceAndSellPrice() { MatchingOrder buyOrder = new MatchingOrder(); buyOrder.setPrice(1000); buyOrder.setCount(23);
MatchingOrder sellOrder = new MatchingOrder(); sellOrder.setPrice(800); sellOrder.setCount(20);
// ⽅法打桩(Method stubbing)
// when(x).thenReturn(y) :当指定⽅法被调⽤时返回指定值
Mockito.when(quoteService.getCurrentPriceByProduct(Mockito.anyString())).thenReturn(900); MatchingResult result = 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
protected void startBuyProcess(MatchingOrder buyOrder, boolean waitForMatching) { while (true) { //对⼿⽅最优价
MatchingOrder topSellOrder = getTopSellOrder(buyOrder.getProductCode()); MatchingResult matchingResult = 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⽅法进⼊下⾯这个判断分⽀后的⾏为 * {@link MatchingServiceImpl#startBuyProcess(MatchingOrder, boolean)} *
*
* */ @Test* if (matchingResult.isSuccess()) { *
* doMatchingSuccess(buyOrder, topSellOrder, matchingResult, MatchingType.BUY); *
* if (buyOrder.getCount() <= 0) { * break; * } * }
*
public void testStartBuyProcess_InCaseOfMatchingSuccess() { MatchingOrder buyOrder = new MatchingOrder(); buyOrder.setPrice(700); buyOrder.setCount(23);
// ⽤Mockito.spy()对matchingService进⾏部分打桩 matchingService = Mockito.spy(matchingService);
MatchingResult firstMatchingResult = new MatchingResult(); firstMatchingResult.setSuccess(true); firstMatchingResult.setTradeCount(20);
MatchingResult secondMatchingResult = new MatchingResult(); 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)); MatchingOrder sellOrder = new MatchingOrder(); 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依赖
springboot+junit+mockito的上下⽂构建MockitoBasedTest
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestApplication.class)public abstract class MockitoBasedTest { @Before
public void setUp() throws Exception {
// 初始化测试⽤例类中由Mockito的注解标注的所有模拟对象 MockitoAnnotations.initMocks(this); }}
// 其他测试类继承MockitoBasedTest
以上就是本⽂的全部内容,希望对⼤家的学习有所帮助,也希望⼤家多多⽀持。
因篇幅问题不能全部显示,请点此查看更多更全内容