Groovy/Spock 测试导论

测试对于软件开发者而言相当重要,不过总会有人说:“写代码是个人事,测试那是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())); } }

提示

  • 最重要也是最容易被遗忘的提示,阅读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测试框架的惟一阻力就剩下大多数开发者是否愿意学习一门新的编程语言。

相关文章
相关标签/搜索