Groovy/Spock 測試導論


測試對於軟件開發者而言至關重要,不過總會有人說:“寫代碼是我的事,測試那是QA的工作”,這樣的想法真是弱爆了,因為大量的業界實踐已經證明測試驅動編碼可以有效地幫助開發者提升代碼質量。

大多數遵循TDD的Java開發者均會使用mockito或powermock,但mockito和powermock均包含了許多樣本代碼,導致測試代碼變得冗長而難以維護。在測試中引入Groovy/Spock后,我完全被它們吸引,並轉向使用Groovy/Spock來替代原有的測試框架。

下面將圍繞一個簡單例子來講解Groovy/Spock,例子中將包含一個service類,負責處理domain對象,以及一個數據訪問層。
首先是domain類:

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等代碼塊

在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())); } }

提示

  • 最重要也是最容易被遺忘的提示,閱讀spock文檔!
  • 可以命名spock塊,例如將given命名為“Some variables”,有助於開發者在測試代碼中更加清楚的表達含義
  • 當對mock對象方法調用次數不關心時,可以使用_ * mock.method()
  • 在then塊中可使用下划線來通配方法及類,例如,0 * mock._ 表示期望mock對象的任何方法都未被調用,或0 * . 表示期望任何對象的任何方法都未被調用
  • 通常按given,when,then編寫測試,但實際上從when開始編寫測試會更加容易發現測試需要的given和測試的輸出結果(then)
  • expect塊對於測試不需要對mock對象進行斷言的簡單方法更加有效
  • 當對於傳遞給mock對象的參數不關注時,可以使用通配符參數
  • 擁抱groovy閉包Embrace groovy closures! They can be you’re best friend in assertions!
  • 當希望在整個測試類中只運行一次,可以復寫setupSpec和cleanupSpec

結論

測試代碼是為了協助開發者的,而不是起相反作用,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測試框架的唯一阻力就剩下大多數開發者是否願意學習一門新的編程語言。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM