基于Springboot+Junit+Mockito做单元测试的⽰例
前⾔
这篇⽂章介绍如何使⽤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)}   *
* 
* if (matchingResult.isSuccess()) {   *
*   doMatchingSuccess(buyOrder, topSellOrder, matchingResult, MatchingType.BUY);   *
*   if (buyOrder.getCount() <= 0) {   *     break;   *   }   * }
* 
   *   */  @Test
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依赖
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)public abstract class MockitoBasedTest {  @Before
public void setUp() throws Exception {
// 初始化测试⽤例类中由Mockito的注解标注的所有模拟对象    MockitoAnnotations.initMocks(this);  }}
// 其他测试类继承MockitoBasedTest
以上就是本⽂的全部内容,希望对⼤家的学习有所帮助,也希望⼤家多多⽀持。