測試對於軟件開發者而言相當重要,不過總會有人說:「寫代碼是個人事,測試那是QA的工做」,這樣的想法真是弱爆了,由於大量的業界實踐已經證實測試驅動編碼能夠有效地幫助開發者提高代碼質量。css
大多數遵循TDD的Java開發者均會使用mockito或powermock,但mockito和powermock均包含了許多樣本代碼,致使測試代碼變得冗長而難以維護。在測試中引入Groovy/Spock後,我徹底被它們吸引,並轉向使用Groovy/Spock來替代原有的測試框架。git
下面將圍繞一個簡單例子來說解Groovy/Spock,例子中將包含一個service類,負責處理domain對象,以及一個數據訪問層。
首先是domain類:github
public class User { private int id; private String name; private int age; // Accessors omitted }
接下來是DAO接口:編程
public interface UserDao { public User get(int id); }
最後是service類:閉包
public class UserService { private UserDao userDao; public UserService(UserDao userDao) { this.userDao = userDao; } public User findUser(int id){ return null; } }
採用Groovy/Spock針對UserService編寫測試框架
class UserServiceTest extends Specification { UserService service UserDao dao = Mock(UserDao) def setup(){ service = new UserService(dao) } def "it gets a user by id"(){ given: def id = 1 when: def result = service.findUser(id) then: 1 * dao.get(id) >> new User(id:id, name:"James", age:27) result.id == 1 result.name == "James" result.age == 27 } }
上述測試代碼中,首先咱們使用了groovy,這是一種很是相似Java的語言,可是它的語法更加輕,例如它不用像Java語言那樣,在每句結尾加上分號;它也不須要使用public修飾符,由於public是默認的。上述測試類繼承自spock.lang.Specification,這是Spock基類,繼承該基類後就可使用given,when,then等代碼塊。dom
在Spock中建立mock對象很是容易,只須要使用Mock(Class)這樣的語句便可。如上所述,mock後的DAO對象被傳入userService中。Setup方法會在每一個測試方法運行前被執行。編程語言
Groovy的一個顯著特色是可使用字符串文原本命名方法,將這個特色應用在測試方法上就能使得測試方法能夠更加容易被閱讀和理解,如上述代碼所示。單元測試
Given, when, then學習
Spock是一個BDD測試框架,所以對於Spock中涉及的given,when,then樣式最簡單的理解就是:
Given 給定一些條件,When 當執行一些操做時,Then 指望獲得某個結果。
如上述測試方法中Given,給定id=1,即測試的變量;而在When中則是被測試方法,如在上述代碼中調用findUser();Then中則是斷言,即檢查被測試方法的輸出結果。
上述Then中的第一句語句雖然看上去可怕,但實際上卻很是容易理解:
1 * dao.get(id) >> new User(id:id, name:"James", age:27)
該行表示了對於mock對象dao的指望值,即指望調用dao.get()方法1次,而「>>」是spock的特點,表示「then return」含義。所以該句翻譯過來的意思是:指望調用1次dao.get()方法,當執行該方法後,請返回一個新的User對象。此外在構造方法中使用具名參數也是groovy的另外一特色。Then中剩餘的代碼對result對象進行檢查。
由此測試代碼驅動產生的產品代碼很是簡單,以下所示:
public class UserService { private UserDao userDao; public UserService(UserDao userDao) { this.userDao = userDao; } public User findUser(int id){ return userDao.get(id); } }
接下來實現建立用戶功能,在UserService中添加以下代碼:
public void createUser(User user){ // check name // if exists, throw exception // if !exists, create user }
在UserDao中添加以下方法:
public User findByName(String name); public void createUser(User user);
相應的測試方法以下:
def "it saves a new user"(){ given: def user = new User(id: 1, name: 'James', age:27) when: service.createUser(user) then: 1 * dao.findByName(user.name) >> null then: 1 * dao.createUser(user) }
在上述代碼中出現了兩處Then,這是由於當全部斷言放在一個then塊中,Spock會認爲這些斷言是同時發生的。若是指望斷言按順序執行,則須要將斷言分割到多個then塊中,spock會按順序執行斷言。如上述所示,首先須要判斷用戶是否存在,而後再去建立用戶。產品代碼實現以下:
public void createUser(User user){ User existing = userDao.findByName(user.getName()); if(existing == null){ userDao.createUser(user); } }
上述代碼針對用戶不存在場景,而對於用戶存在的場景,測試代碼以下:
def "it fails to create a user because one already exists with that name"(){ given: def user = new User(id: 1, name: 'James', age:27) when: service.createUser(user) then: 1 * dao.findByName(user.name) >> user then: 0 * dao.createUser(user) then: def exception = thrown(RuntimeException) exception.message == "User with name ${user.name} already exists!" }
上述代碼當調用findByName時,返回一個存在的用戶,而後不調用createUser(),第三個Then塊捕獲方法拋出的異常。注意groovy擁有一個稱之爲GStrings的特徵,該特徵能夠在引用的字符串中插入參數,如${user.name}。相應產品代碼以下:
public void createUser(User user){ User existing = userDao.findByName(user.getName()); if(existing == null){ userDao.createUser(user); } else{ throw new RuntimeException(String.format("User with name %s already exists!", user.getName())); } }
提示
結論
測試代碼是爲了協助開發者的,而不是起相副作用,groovy在這方面提供了不少快捷方式來幫助開發者寫出更加優雅的測試代碼。完整代碼可參考https://gist.github.com/jameselsey/8096211
思考
翻譯這篇文章是受到了《使用 Groovy 語言替代 JUnit 來爲 Java 程序編寫單元測試》和《The Coding Kata: FizzBuzzWhizz in Modern Java》兩篇文章的啓示。除了讚歎兩篇文章中採用的測試框架的易用,也深深地被groovy所吸引,其做爲DSL的特質不管是對於追求編寫更好測試用例的精益開發者仍是對於剛入門測試用例的新手開發者來講都是容易掌握和使用的。咱們指望測試用例的目標就是可以做爲產品代碼的 living docs,最佳的效果就是徹底擺脫編程語言的語法束縛,成爲純粹的書寫或口頭表達方式,這樣就能「望文生義」。Groovy在這方面確實對於Java測試用例編寫起到了促進做用,再加上groovy與Java的無縫融合,及自身擁有的語法特性,在團隊中推廣groovy替代傳統Java測試框架的惟一阻力就剩下大多數開發者是否願意學習一門新的編程語言。