前言
这篇文章介绍如何使用Springboot+Junit+Mockito做单元测试,案例选取撮合交易的一个类来做单元测试。
单元测试前先理解需求
要写出好的单测,必须先理解了需求,只有知道做什么才能知道怎么测。但本文主要讲mockito的用法,无需关注具体需求。所以本节略去具体的需求描述。
隔离外部依赖
Case1. 被测类中被@Autowired 或 @Resource 注解标注的依赖对象,如何控制其返回值
以被测方法 MatchingServiceImpl.java的matching(MatchingOrder buyOrder, MatchingOrder sellOrder)为例
被测类MatchingServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public class MatchingServiceImplTest extends MockitoBasedTest { /** * 被@Mock标注的对象会自动注入到被@InjectMocks标注的对象中 */ @Mock private QuoteService quoteService; /** * <pre> * 被测对象,用@InjectMocks标注,那些被@mock标注的对象就会自动注入其中。 * 另一个注意点是这里的MatchingServiceImpl是直接new出来(Mockito 1.9版本后不new也可以),而不是通过spring容器注入的。因为这里我不需要从spring容器中 * 获得其他依赖,不需要database ,redis ,zookeeper,mq,啥都不依赖,所以直接new * </pre> */ @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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
/** * * 测试StartBuyProcess方法在撮合成功后的处理是否符合预期,即测试startBuyProcess方法进入下面这个判断分支后的行为 * {@link MatchingServiceImpl#startBuyProcess(MatchingOrder, boolean)} * * <pre> * if (matchingResult.isSuccess()) { * * doMatchingSuccess(buyOrder, topSellOrder, matchingResult, MatchingType.BUY); * * if (buyOrder.getCount() <= 0) { * break; * } * } * </pre> * */ @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依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
< dependency > < groupId >junit</ groupId > < artifactId >junit</ artifactId > < version >4.11</ version > < scope >test</ scope > </ dependency > < dependency > < groupId >org.mockito</ groupId > < artifactId >mockito-all</ artifactId > < version >1.10.19</ version > < scope >test</ scope > </ dependency > < dependency > < groupId >org.springframework</ groupId > < artifactId >spring-test</ artifactId > < version >4.2.5.RELEASE</ version > < scope >test</ scope > </ dependency > |
springboot+junit+mockito的上下文构建
MockitoBasedTest
1
2
3
4
5
6
7
8
9
10
|
@RunWith (SpringJUnit4ClassRunner. class ) @SpringApplicationConfiguration (classes = TestApplication. class ) public abstract class MockitoBasedTest { @Before public void setUp() throws Exception { // 初始化测试用例类中由Mockito的注解标注的所有模拟对象 MockitoAnnotations.initMocks( this ); } } // 其他测试类继承MockitoBasedTest |
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:http://blog.csdn.net/flysqrlboy/article/details/79301356