基于Springboot+Junit+Mockito做單元測試的示例
前言
這篇文章介紹如何使用Springboot+Junit+Mockito做單元測試,案例選取撮合交易的一個類來做單元測試。
單元測試前先理解需求
要寫出好的單測,必須先理解了需求,只有知道做什么才能知道怎么測。但本文主要講mockito的用法,無需關(guān)注具體需求。所以本節(jié)略去具體的需求描述。
隔離外部依賴
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; /** * <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) :當指定方法被調(diào)用時返回指定值 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()); // 斷言最新報價是否符合預(yù)期 }
Case2. 被測函數(shù)A調(diào)用被測類其他函數(shù)B,怎么控制函數(shù)B的返回值?
比如,MatchingServiceImpl中有個函數(shù)startBuyProcess,它里面調(diào)用了該類中的其他函數(shù),如getTopSellOrder,matching,如何控制這兩個函數(shù)的返回值?
這里要解決的問題其實是怎么對一個類”部分mock”–被測類的被測方法(如startBuyProcess)要真實執(zhí)行,而另一些方法(如getTopSellOrder)則是要打樁(不真正進去執(zhí)行)。
被測類MatchingServiceImpl
protected void startBuyProcess(MatchingOrder buyOrder, boolean waitForMatching) { while (true) { //對手方最優(yōu)價 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方法在撮合成功后的處理是否符合預(yù)期,即測試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() 對方法打樁,打樁后,程序執(zhí)行這些方法時將按照預(yù)期返回指定值,未被打樁的方法將真實執(zhí)行 // 兩個doReturn表示第一次調(diào)用matchingService.matching時返回firstMatchingResult,第二次調(diào)用返回secondMatchingResult // 因為startBuyProcess里有個while循壞,可能會多次執(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); // 對getTopSellOrder方法打樁 Mockito.doReturn(sellOrder).when(matchingService).getTopSellOrder(Mockito.anyString()); // 對外部依賴jedis的方法進行打樁 Mockito.when(jedisClient.incrBy(Mockito.anyString(), Mockito.anyLong())).thenReturn(0L); // startBuyProcess是被測函數(shù),不打樁,會真實執(zhí)行 matchingService.startBuyProcess(buyOrder, true); // 后面的校驗和斷言是測試doMatchingSuccess方法的行為的,這也是這個測試的目的 // verify可用來校驗,某個類的方法被執(zhí)行過多少次,這里是校驗jedisClient.zremFirst是否被執(zhí)行過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說下單元測試的“粒度”。
testStartBuyProcess_InCaseOfMatchingSuccess的目的是想測doMatchingSuccess,我們費了很大勁才把前面的一堆準備工作做完,才能去測doMatchingSuccess。
更好的實踐應(yīng)該是另起測試方法去單獨測doMatchingSuccess,關(guān)注點也集中很多,doMatchingSuccess覆蓋完了,再測startBuyProcess其實就只是覆蓋下它本身的判斷分支就行了。覆蓋率照樣達到,而且測試代碼也更容易維護,testStartBuyProcess_InCaseOfMatchingSuccess由于考慮的職責太多,它很容易受到變化的影響,細小的東西改變,可能就會影響它的正常工作。
引入測試框架Maven依賴
<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
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = TestApplication.class) public abstract class MockitoBasedTest { @Before public void setUp() throws Exception { // 初始化測試用例類中由Mockito的注解標注的所有模擬對象 MockitoAnnotations.initMocks(this); } } // 其他測試類繼承MockitoBasedTest
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Spring動態(tài)添加定時任務(wù)的實現(xiàn)思路
這篇文章主要介紹了Spring動態(tài)添加定時任務(wù)的實現(xiàn)思路,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-07-07SpringBoot2如何集成Elasticsearch6.x(TransportClient方式)
這篇文章主要介紹了SpringBoot2如何集成Elasticsearch6.x(TransportClient方式)問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05解決Maven打包只有幾十K,運行報錯no main manifest attribute
這篇文章主要介紹了解決Maven打包只有幾十K,運行報錯no main manifest attribute問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06WIN10系統(tǒng)中添加bat腳本重啟jar服務(wù)
在bat腳本中執(zhí)行java服務(wù),命令與cmd中類似,下面這篇文章主要給大家介紹了關(guān)于WIN10系統(tǒng)中添加bat腳本重啟jar服務(wù)的相關(guān)資料,文中通過圖文介紹的非常詳細,需要的朋友可以參考下2023-12-12一篇文章教你將JAVA的RabbitMQz與SpringBoot整合
這篇文章主要介紹了如何將JAVA的RabbitMQz與SpringBoot整合,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2021-09-09