單元測試是保證項目代碼質量的有力武器,但是有些業務場景,依賴的第三方沒有測試環境,這時候該怎么做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 <artifactId>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>
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>
這樣就把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元,不應該為Null對象,同時還輸出了一行日志,但是從測試結果來看,這個方法並沒有真正執行。這也就印證了@Mock修飾的對象,是“假”的,並不會真正執行內部的代碼
@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的返回值
現在測試就通過了,再看看生成的測試報告,最后幾行,也被覆蓋到了。







