前言
這篇文章介紹如何使用Springboot+Junit+Mockito做單元測(cè)試,案例選取撮合交易的一個(gè)類來(lái)做單元測(cè)試。
單元測(cè)試前先理解需求
要寫出好的單測(cè),必須先理解了需求,只有知道做什么才能知道怎么測(cè)。但本文主要講mockito的用法,無(wú)需關(guān)注具體需求。所以本節(jié)略去具體的需求描述。
隔離外部依賴
Case1. 被測(cè)類中被@Autowired 或 @Resource 注解標(biāo)注的依賴對(duì)象,如何控制其返回值
以被測(cè)方法 MatchingServiceImpl.java的matching(MatchingOrder buyOrder, MatchingOrder sellOrder)為例
被測(cè)類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獲取當(dāng)前報(bào)價(jià),這里我們需要把外部依賴quoteService mock掉,控制getCurrentPriceByProduct方法的返回值。使用mockito可以做到,具體如下:
測(cè)試類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標(biāo)注的對(duì)象會(huì)自動(dòng)注入到被@InjectMocks標(biāo)注的對(duì)象中 */ @Mock private QuoteService quoteService; /** * <pre> * 被測(cè)對(duì)象,用@InjectMocks標(biāo)注,那些被@mock標(biāo)注的對(duì)象就會(huì)自動(dòng)注入其中。 * 另一個(gè)注意點(diǎn)是這里的MatchingServiceImpl是直接new出來(lái)(Mockito 1.9版本后不new也可以),而不是通過(guò)spring容器注入的。因?yàn)檫@里我不需要從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) :當(dāng)指定方法被調(diào)用時(shí)返回指定值 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()); // 斷言成交數(shù)量 org.junit.Assert.assertEquals( 900 , result.getTradePrice()); // 斷言最新報(bào)價(jià)是否符合預(yù)期 } |
Case2. 被測(cè)函數(shù)A調(diào)用被測(cè)類其他函數(shù)B,怎么控制函數(shù)B的返回值?
比如,MatchingServiceImpl中有個(gè)函數(shù)startBuyProcess,它里面調(diào)用了該類中的其他函數(shù),如getTopSellOrder,matching,如何控制這兩個(gè)函數(shù)的返回值?
這里要解決的問題其實(shí)是怎么對(duì)一個(gè)類”部分mock”–被測(cè)類的被測(cè)方法(如startBuyProcess)要真實(shí)執(zhí)行,而另一些方法(如getTopSellOrder)則是要打樁(不真正進(jìn)去執(zhí)行)。
被測(cè)類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 ) { //對(duì)手方最優(yōu)價(jià) 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) { //加入待撮合隊(duì)列 addToMatchingBuy(buyOrder); } else { //撤單 sendCancleMsg(buyOrder); } break ; } } } |
利用Mockito.spy()可以做到“部分Mock”
測(cè)試類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
|
/** * * 測(cè)試StartBuyProcess方法在撮合成功后的處理是否符合預(yù)期,即測(cè)試startBuyProcess方法進(jìn)入下面這個(gè)判斷分支后的行為 * {@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()對(duì)matchingService進(jìn)行部分打樁 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() 對(duì)方法打樁,打樁后,程序執(zhí)行這些方法時(shí)將按照預(yù)期返回指定值,未被打樁的方法將真實(shí)執(zhí)行 // 兩個(gè)doReturn表示第一次調(diào)用matchingService.matching時(shí)返回firstMatchingResult,第二次調(diào)用返回secondMatchingResult // 因?yàn)閟tartBuyProcess里有個(gè)while循壞,可能會(huì)多次執(zhí)行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 ); // 對(duì)getTopSellOrder方法打樁 Mockito.doReturn(sellOrder).when(matchingService).getTopSellOrder(Mockito.anyString()); // 對(duì)外部依賴jedis的方法進(jìn)行打樁 Mockito.when(jedisClient.incrBy(Mockito.anyString(), Mockito.anyLong())).thenReturn(0L); // startBuyProcess是被測(cè)函數(shù),不打樁,會(huì)真實(shí)執(zhí)行 matchingService.startBuyProcess(buyOrder, true ); // 后面的校驗(yàn)和斷言是測(cè)試doMatchingSuccess方法的行為的,這也是這個(gè)測(cè)試的目的 // verify可用來(lái)校驗(yàn),某個(gè)類的方法被執(zhí)行過(guò)多少次,這里是校驗(yàn)jedisClient.zremFirst是否被執(zhí)行過(guò)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的用法已經(jīng)演示完畢,下面從testStartBuyProcess_InCaseOfMatchingSuccess說(shuō)下單元測(cè)試的“粒度”。
testStartBuyProcess_InCaseOfMatchingSuccess的目的是想測(cè)doMatchingSuccess,我們費(fèi)了很大勁才把前面的一堆準(zhǔn)備工作做完,才能去測(cè)doMatchingSuccess。
更好的實(shí)踐應(yīng)該是另起測(cè)試方法去單獨(dú)測(cè)doMatchingSuccess,關(guān)注點(diǎn)也集中很多,doMatchingSuccess覆蓋完了,再測(cè)startBuyProcess其實(shí)就只是覆蓋下它本身的判斷分支就行了。覆蓋率照樣達(dá)到,而且測(cè)試代碼也更容易維護(hù),testStartBuyProcess_InCaseOfMatchingSuccess由于考慮的職責(zé)太多,它很容易受到變化的影響,細(xì)小的東西改變,可能就會(huì)影響它的正常工作。
引入測(cè)試框架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的上下文構(gòu)建
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 { // 初始化測(cè)試用例類中由Mockito的注解標(biāo)注的所有模擬對(duì)象 MockitoAnnotations.initMocks( this ); } } // 其他測(cè)試類繼承MockitoBasedTest |
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持服務(wù)器之家。
原文鏈接:http://blog.csdn.net/flysqrlboy/article/details/79301356