在上一篇講單元測試代碼可讀性和維護性的問題時舉了一種業務場景,即接口調用,咱們的用戶服務須要調用用戶中心接口獲取用戶信息,代碼以下:html
/** * 用戶服務 * @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轉換,郵編、手機號處理等)java
因此正常的作法是把userDao的getUserInfo()
方法mock掉,模擬一個咱們指定的值,由於咱們真正關心的是拿到users後本身代碼的邏輯,這是咱們須要重點驗證的地方spring
按照上面的思路使用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的代碼:(從上往下)ide
def userDao = Mock(UserDao)
這一行代碼使用spock自帶的Mock方法構造一個userDao的mock對象,若是要模擬userDao方法的返回,只需userDao.方法名() >> 模擬值
的方式,兩個右箭頭的方式便可函數
setup
方法是每一個測試用例運行前的初始方法,相似於junit的@before
post
GetUserById
方法是單測的主要方法,能夠看到分爲4個模塊:given
、and
、when
、then
,用來區分不一樣單測代碼的做用:單元測試
每一個標籤後面的雙引號裏能夠添加描述,說明這塊代碼的做用(非強制),如"when: "調用獲取用戶信息方法""測試
由於spock使用groovy做爲單測開發語言,因此代碼量上比使用java寫的會少不少,好比given模塊裏經過構造函數的方式建立請求對象ui
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
的方式更簡單一些
上面的業務代碼有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獲取)