mock測試及jacoco覆蓋率
單元測試是保證專案程式碼質量的有力武器,但是有些業務場景,依賴的第三方沒有測試環境,這時候該怎麼做Unit Test呢,總不能直接生產環境硬來吧?
可以藉助一些mock測試工具,比如下面要講的mockito,廢話不多說,直奔主題:
一、準備示例Demo
假設有一個訂單系統,使用者可以建立訂單,同時下單後要檢測使用者餘額(如果餘額不足,提醒使用者充值),具體來說,裡面有2個服務:OrderService、UserService,類圖如下:
示例程式碼:
package com.cnblogs.yjmyzz.springbootdemo.service.impl; import com.cnblogs.yjmyzz.springbootdemo.service.UserService; import org.springframework.stereotype.Service; import java.math.BigDecimal; /** * @author 菩提樹下的楊過 */ @Service("userService") public class UserServiceImpl implements UserService { @Override public BigDecimal queryBalance(int userId) { System.out.println("queryBalance=>userId:" + userId); //模擬返回100元餘額 return new BigDecimal(100); } }
及
package com.cnblogs.yjmyzz.springbootdemo.service.impl; import com.cnblogs.yjmyzz.springbootdemo.service.OrderService; import com.cnblogs.yjmyzz.springbootdemo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.math.BigDecimal; @Service("orderService") public class OrderServiceImpl implements OrderService { @Autowired private UserService userService; /** * 下訂單 * * @param productName * @param orderNum * @return * @throws Exception */ @Override public Long createOrder(String productName, Integer orderNum, int userId) throws Exception { System.out.println("createOrder=>userId:" + userId); if (StringUtils.isEmpty(productName)) { throw new Exception("productName is empty"); } if (orderNum == null) { throw new Exception("orderNum is null!"); } if (orderNum <= 0) { throw new Exception("orderNum must bigger than 0"); } //下訂單過程略,返回1L做為訂單號 Long orderId = 1L; //模擬檢測餘額 BigDecimal balance = userService.queryBalance(userId); if (balance.compareTo(BigDecimal.TEN) <= 0) { System.out.println("餘額不足10元,請及時充值!"); } return orderId; } }
裡面的邏輯不是重點,隨便看看就好。關注下createOrder方法,最後幾行OrderService服務呼叫了UserService服務查詢使用者餘額,即:OrderService依賴UserService,假設UserService這個服務,就是一個第3方服務,不具備測試環境,本文就來講講如何對UserService進行mock測試。
二、pom引入mockito 及jacoco plugin
2.1引入mockito
1 <dependency> 2 <groupId>org.mockito</groupId> 3 <artifactIdView Code>mockito-all</artifactId> 4 <version>1.9.5</version> 5 <scope>test</scope> 6 </dependency>
mockito是一個mock工具庫,馬上會講到用法。
2.2引入jacoco外掛
1 <plugin> 2 <groupId>org.jacoco</groupId> 3 <artifactId>jacoco-maven-plugin</artifactId> 4 <version>0.8.5</version> 5 <executions> 6 <execution> 7 <id>prepare-agent</id> 8 <goals> 9 <goal>prepare-agent</goal> 10 </goals> 11 </execution> 12 <execution> 13 <id>report</id> 14 <phase>prepare-package</phase> 15 <goals> 16 <goal>report</goal> 17 </goals> 18 </execution> 19 <execution> 20 <id>post-unit-test</id> 21 <phase>test</phase> 22 <goals> 23 <goal>report</goal> 24 </goals> 25 <configuration> 26 <dataFile>target/jacoco.exec</dataFile> 27 <outputDirectory>target/jacoco-ut</outputDirectory> 28 </configuration> 29 </execution> 30 </executions> 31 </plugin>View Code
jacoco可以將單元測試的結果,直接生成html網頁,分析程式碼覆蓋率。注意<outputDirectory>target/jacoco-ut</outputDirectory> 這一行的配置,表示將在target/jacoco-ut目錄下生成測試報表。
三、編寫單測用例
3.1約定大於規範
以OrderServiceImpl類為例,如果要對它做單元測試,建議按以下約定:
a. 在test/java下建立一個與OrderServiceImpl同名的package名(注:這樣的好處是測試類與原類,處於同1個包,程式碼可見性相同)
b. 然後在該package下建立OrderServiceImplTest類(注意:一般測試類名的風格為 xxxxTest,在原類名後加Test)
3.2 單元測試模板
參考下面的程式碼模板:
package com.cnblogs.yjmyzz.springbootdemo.service.impl; import com.cnblogs.yjmyzz.springbootdemo.service.UserService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.runners.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class OrderServiceImplTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); } /** * 真正要測試的類 */ @InjectMocks private OrderServiceImpl orderService; /** * 測試類依賴的其它服務 */ @Mock private UserService userService; /** * createOrder成功時的用例 */ @Test public void testCreateOrderSuccess() { //todo } /** * createOrder失敗時的用例 */ @Test public void testCreateOrderFailure() { //todo } }
講解一下:
a. 類上的@RunWith要改成MockitoJUnitRunner.class,否則mockito不生效
b. 真正需要測試的類,要用@InjectMocks,而不是@Mock(更不能是@Autowired)
-- 原因1:@Autowired是Spring的註解,在mock環境下,根本就沒有Spring上下文,當然會注入失敗。
-- 原因2:也不能是@Mock,@Mock表示該注入的物件是“偽假”的,裡面的方法程式碼根本不會真正執行,統一返回空物件null,即:被@Mock修飾的物件,在該測試類中,其具體的程式碼永遠無法覆蓋到!這也就是失敗了單元測試的意義。而@InjectMocks修飾的物件,被測試的方法,才會真正進入執行。
另外,測試服務時,被mock注入的類,應該是具體的服務實現類,即:xxxServiceImpl,而不是服務介面,在mock環境中介面是無法例項化的。
c. 通常一個方法,會有執行成功和執行失敗二種情況,建議測試類裡,用testXXXSuccess以及testXXXFailure區分開來,看起來比較清晰。
3.3 測試覆蓋率
先來看看下單失敗的情況:下單前有很多引數校驗,先驗證下這些引數異常的場景。
public int userId = 101; /** * createOrder失敗時的用例 */ @Test public void testCreateOrderWhenFail() { try { orderService.createOrder(null, 10, userId); } catch (Exception e) { Assert.assertEquals(true, true); } try { orderService.createOrder("book", null, userId); } catch (Exception e) { Assert.assertEquals(true, true); } try { orderService.createOrder("book", 0, userId); } catch (Exception e) { Assert.assertEquals(true, true); } try { orderService.createOrder("book", 50, userId); } catch (Exception e) { Assert.assertEquals(true, true); } }
命令列下mvn package 跑一下單元測試,全通過後,會在target/jacoco-ut 目錄下生成報表網頁
瀏覽器開啟index.html,就能看到覆蓋率
可以看到,中間那個帶部分綠色的,就是我們剛才寫過單測的pacakge,一層層點下去,可以看到最終OrderServiceImpl.createOrder方法的程式碼覆蓋情況,綠色的行表示覆蓋到了,紅色的表示未覆蓋到
講一個小技巧:有些類,比如DAO/Mytatis層自動生成的DO/Entity這些類,還有一些常量定義等,其實沒什麼測試的必要,可以排除掉,這樣不僅可以提高測試的覆蓋率,還能讓我們更關注於核心業務類的測試。
排除的方法很簡單,可jacoco外掛裡配置exclude規則即可,參考下面這樣:
<configuration> <dataFile>target/jacoco.exec</dataFile> <outputDirectory>target/jacoco-ut</outputDirectory> <excludes> <exclude> **/cnblogs/yjmyzz/**/aspect/**, **/yjmyzz/**/SampleApplication.class </exclude> </excludes> </configuration>View Code
這樣就把aspect包下的所有類,以及SampleApplication.class這個特定類給排除在單元測試之外,此時再跑一下mvn package ,對比下重新生成的報告
覆蓋率從剛才的26%上升到了61%
3.4 mock返回值
從覆蓋率上看,剛才createOrder方法裡,最後幾行並沒有覆蓋到,可以再寫一個用例
問題來了,UserService的queryBalance方法
@Override public BigDecimal queryBalance(int userId) { System.out.println("queryBalance=>userId:" + userId); //模擬返回100元餘額 return new BigDecimal(100); }
已經寫死了返回100元,同時還輸出了一行日誌,但是從測試結果來看,這個方法並沒有真正執行。這也就印證了@Mock修飾的物件,是“假”的,並不會真正執行內部的程式碼,只會返回null物件。
@Test public void testCreateOrderSuccess() throws Exception { BigDecimal balance = BigDecimal.TEN; //表示:當userService.queryBalance(userId)執行時,將返回balance變數做為返回值 when(userService.queryBalance(userId)).thenReturn(balance); long orderId = orderService.createOrder("phone", 10, userId); Assert.assertEquals(orderId, 1L); }
把測試程式碼調整下,改成上面這樣,利用when(...).thenReturn(...),表示當xxx方法執行時,將模擬返回yyy物件。這樣就mock出了userService的返回值
現在測試就通過了,再看看生成的測試報告,最後幾行,也被覆蓋到了。