1. 程式人生 > >單元測試系列三-基於Springboot+Junit+Mockito做單元測試

單元測試系列三-基於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;
    /**
     * <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
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)}
     *
     * <pre>
     * if (matchingResult.isSuccess()) {
     *
     *     doMatchingSuccess(buyOrder, topSellOrder, matchingResult, MatchingType.BUY);
     *
     *     if (buyOrder.getCount() &lt;= 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依賴

<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
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestApplication.class)
public abstract class MockitoBasedTest {
    @Before
    public void setUp() throws Exception {
        // 初始化測試用例類中由Mockito的註解標註的所有模擬物件
        MockitoAnnotations.initMocks(this);
    }
}
// 其他測試類繼承MockitoBasedTest