Spock自帶的Mock用法
在上一篇講單元測試代碼可讀性和維護性的問題時舉了一種業務場景,即接口調用,我們的用戶服務需要調用用戶中心接口獲取用戶信息,代碼如下:
/**
* 用戶服務
* @author 公眾號:Java老K
* 個人博客:www.javakk.com
*/
@Service
public class UserService {
@Autowired
UserDao userDao;
@Autowired
MoneyDAO moneyDAO;
public UserVO getUserById(int uid){
List<UserDTO> users = userDao.getUserInfo();
UserDTO userDTO = users.stream().filter(u -> u.getId() == uid).findFirst().orElse(null);
UserVO userVO = new UserVO();
if(null == userDTO){
return userVO;
}
userVO.setId(userDTO.getId());
userVO.setName(userDTO.getName());
userVO.setSex(userDTO.getSex());
userVO.setAge(userDTO.getAge());
// 顯示郵編
if("上海".equals(userDTO.getProvince())){
userVO.setAbbreviation("滬");
userVO.setPostCode(200000);
}
if("北京".equals(userDTO.getProvince())){
userVO.setAbbreviation("京");
userVO.setPostCode(100000);
}
// 手機號處理
if(null != userDTO.getTelephone() && !"".equals(userDTO.getTelephone())){
userVO.setTelephone(userDTO.getTelephone().substring(0,3)+"****"+userDTO.getTelephone().substring(7));
}
return userVO;
}
}
其中userDao
是使用spring注入的用戶中心服務的實例對象,我們只有拿到了用戶中心的返回的users
,才能繼續下面的邏輯(根據uid篩選用戶,DTO和VO轉換,郵編、手機號處理等)
所以正常的做法是把userDao的getUserInfo()
方法mock掉,模擬一個我們指定的值,因為我們真正關心的是拿到users后自己代碼的邏輯,這是我們需要重點驗證的地方
按照上面的思路使用Spock編寫的測試代碼如下:
package com.javakk.spock.service
import com.javakk.spock.dao.UserDao
import spock.lang.Specification
import spock.lang.Unroll
/**
* 用戶服務測試類
* @author 公眾號:Java老K
* 個人博客:www.javakk.com
*/
class UserServiceTest extends Specification {
def userService = new UserService()
def userDao = Mock(UserDao)
void setup() {
userService.userDao = userDao
}
def "GetUserById"() {
given: "設置請求參數"
def user1 = new UserDTO(id:1, name:"張三", province: "上海")
def user2 = new UserDTO(id:2, name:"李四", province: "江蘇")
and: "mock掉接口返回的用戶信息"
userDao.getUserInfo() >> [user1, user2]
when: "調用獲取用戶信息方法"
def response = userService.getUserById(1)
then: "驗證返回結果是否符合預期值"
with(response) {
name == "張三"
abbreviation == "滬"
postCode == 200000
}
}
}
如果要看junit如何實現可以參考上一篇的對比圖,這里主要講解spock的代碼:(從上往下)
def userDao = Mock(UserDao)
這一行代碼使用spock自帶的Mock方法構造一個userDao的mock對象,如果要模擬userDao方法的返回,只需userDao.方法名() >> 模擬值
的方式,兩個右箭頭的方式即可
setup
方法是每個測試用例運行前的初始方法,類似於junit的@before
GetUserById
方法是單測的主要方法,可以看到分為4個模塊:given
、and
、when
、then
,用來區分不同單測代碼的作用:
- given: 輸入條件(前置參數)
- when: 執行行為(mock接口、真實調用)
- then: 輸出條件(驗證結果)
- and: 銜接上個標簽,補充的作用
每個標簽后面的雙引號里可以添加描述,說明這塊代碼的作用(非強制),如"when: "調用獲取用戶信息方法""
因為spock使用groovy作為單測開發語言,所以代碼量上比使用java寫的會少很多,比如given模塊里通過構造函數的方式創建請求對象
given: "設置請求參數"
def user1 = new UserDTO(id:1, name:"張三", province: "上海")
def user2 = new UserDTO(id:2, name:"李四", province: "江蘇")
實際上UserDTO.java
這個類並沒有3個參數的構造函數,是groovy幫我們實現的,groovy默認會提供一個包含所有對象屬性的構造函數
而且調用方式上可以指定屬性名,類似於key:value的語法,非常人性化,方便我們在屬性多的情況下構造對象,如果使用java寫,可能就要調用很多setXXX()
方法才能完成對象初始化的工作
and: "mock掉接口返回的用戶信息"
userDao.getUserInfo() >> [user1, user2]
這個就是spock的mock用法,即當調用userDao.getUserInfo()
方法時返回一個List,list的創建也很簡單,中括號"[]"即表示list,groovy會根據方法的返回類型自動匹配是數組還是list,而list里的對象就是之前given塊里構造的user對象
其中 ">>" 就是指定返回結果,類似mockito的when().thenReturn()
語法,但更簡潔一些
如果要指定返回多個值的話可以使用3個右箭頭">>>",比如:
userDao.getUserInfo() >>> [[user1,user2],[user3,user4],[user5,user6]]
也可以寫成這樣:
userDao.getUserInfo() >> [user1,user2] >> [user3,user4] >> [user5,user6]
即每次調用userDao.getUserInfo()
方法返回不同的值
如果mock的方法帶有入參的話,比如下面的業務代碼:
public List<UserDTO> getUserInfo(String uid){
// 模擬用戶中心服務接口調用
List<UserDTO> users = new ArrayList<>();
return users;
}
這個getUserInfo(String uid)
方法,有個參數uid,這種情況下如果使用spock的mock模擬調用的話,可以使用下划線"_"匹配參數,表示任何類型的參數,多個逗號隔開,類似與mockito的any()
方法
如果類中存在多個同名函數,可以通過 "_ as 參數類型" 的方式區別調用,類似下面的語法:
// _ 表示匹配任意類型參數
List<UserDTO> users = userDao.getUserInfo(_);
// 如果有同名的方法,使用as指定參數類型區分
List<UserDTO> users = userDao.getUserInfo(_ as String);
when模塊里是真正調用要測試方法的入口:userService.getUserById()
then模塊作用是驗證被測方法的結果是否正確,符合預期值,所以這個模塊里的語句必須是boolean表達式,類似於junit的assert斷言機制,但你不必顯示的寫assert,這也是一種約定優於配置的思想
then
塊中使用了spock的with
功能,可以驗證返回結果response對象內部的多個屬性是否符合預期值,這個相對於junit的assertNotNull
或assertEquals
的方式更簡單一些
Where用法
上面的業務代碼有3個if判斷,分別是對郵編和手機號的處理邏輯:
// 顯示郵編
if("上海".equals(userDTO.getProvince())){
userVO.setAbbreviation("滬");
userVO.setPostCode(200000);
}
if("北京".equals(userDTO.getProvince())){
userVO.setAbbreviation("京");
userVO.setPostCode(100000);
}
// 手機號處理
if(null != userDTO.getTelephone() && !"".equals(userDTO.getTelephone())){
userVO.setTelephone(userDTO.getTelephone().substring(0,3)+"****"+userDTO.getTelephone().substring(7));
}
現在的單元測試如果要完全覆蓋這3個分支就需要構造不同的請求參數多次調用被測試方法才能走到不同的分支,在上一篇中介紹了spock的where
標簽可以很方便的實現這種功能,代碼如下:
@Unroll
def "當輸入的用戶id為:#uid 時返回的郵編是:#postCodeResult,處理后的電話號碼是:#telephoneResult"() {
given: "mock掉接口返回的用戶信息"
userDao.getUserInfo() >> users
when: "調用獲取用戶信息方法"
def response = userService.getUserById(uid)
then: "驗證返回結果是否符合預期值"
with(response) {
postCode == postCodeResult
telephone == telephoneResult
}
where: "表格方式驗證用戶信息的分支場景"
uid | users || postCodeResult | telephoneResult
1 | getUser("上海", "13866667777") || 200000 | "138****7777"
1 | getUser("北京", "13811112222") || 100000 | "138****2222"
2 | getUser("南京", "13833334444") || 0 | null
}
def getUser(String province, String telephone){
return [new UserDTO(id: 1, name: "張三", province: province, telephone: telephone)]
}
where
模塊第一行代碼是表格的列名,多個列使用"|"單豎線隔開,"||"雙豎線區分輸入和輸出變量,即左邊是輸入值,右邊是輸出值
格式如下:
輸入參數1 | 輸入參數2 || 輸出結果1 | 輸出結果2
而且intellij idea支持format格式化快捷鍵,因為表格列的長度不一樣,手動對齊比較麻煩
表格的每一行代表一個測試用例,即被測方法被測試了3次,每次的輸入和輸出都不一樣,剛好可以覆蓋全部分支情況
比如uid、users都是輸入條件,其中users對象的構造調用了getUser
方法,每次測試業務代碼傳入不同的user值,postCodeResult
、telephoneResult
表示對返回的response對象的屬性判斷是否正確
第一行數據的作用是驗證返回的郵編是否是"200000",第二行是驗證郵編是否是"100000",第三行的郵編是否是"0"(因為代碼里沒有對南京的郵編進行處理,所以默認值是0)
這個就是where
+with
的用法,更符合我們實際測試的場景,既能覆蓋多種分支,又可以對復雜對象的屬性進行驗證
其中在第2行定義的測試方法名是使用了groovy的字面值特性:
@Unroll
def "當輸入的用戶id為:#uid 時返回的郵編是:#postCodeResult,處理后的電話號碼是:#telephoneResult"() {
即把請求參數值和返回結果值的字符串里動態替換掉,"#uid、#postCodeResult、#telephoneResult" 井號后面的變量是在方法內部定義的,前面加上#號,實現占位符的功能
@Unroll
注解,可以把每一次調用作為一個單獨的測試用例運行,這樣運行后的單測結果更直觀:
而且其中一行測試結果不對,spock的錯誤提示信息也很詳細,方便排查(比如我們把第2條測試用例返回的郵編改成"100001"):
可以看出第2條測試用例失敗,錯誤信息是postCodeResult
的預期結果和實際結果不符,業務代碼邏輯返回的郵編是"100000",而我們預期的郵編是"100001",這樣你就可以排查是業務代碼邏輯有問題還是我們的斷言不對。
通過這個例子大家可以看到Spock結合groovy語言在測試多個分支場景時的優勢。
(完整的源碼在公眾號【java老k】里回復spock獲取)