Groovy/Spock 测试导论

原文 http://java.dzone.com/articles/intro-so-groovyspock-testing

翻译 hxfirefox

测试对于软件开发者而言至关重要,不过总会有人说:“写代码是我的事,测试那是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测试框架的唯一阻力就剩下大多数开发者是否愿意学习一门新的编程语言。

Groovy/Spock 测试导论的更多相关文章

  1. Groovy Spock环境的安装

    听说spock是一个加强版的Junit,今天特地安装了,再把过程给大家分享一下. 首先说明,我的Java项目是用maven管理的. 我用的Eclipse是Kelper,开普勒. 要使用Spock之前, ...

  2. 使用Groovy+Spock轻松写出更简洁的单测

    当无法避免做一件事时,那就让它变得更简单. 概述 单测是规范的软件开发流程中的必不可少的环节之一.再伟大的程序员也难以避免自己不犯错,不写出有BUG的程序.单测就是用来检测BUG的.Java阵营中,J ...

  3. 使用Groovy+Spock构建可配置的订单搜索接口测试用例集

    概述 测试是软件成功上线的安全网.基本的测试包含单元测试.接口测试.在 "使用Groovy+Spock轻松写出更简洁的单测" 一文中已经讨论了使用GroovySpock编写简洁的单 ...

  4. 在Spring Boot项目中使用Spock测试框架

    本文首发于个人网站:在Spring Boot项目中使用Spock测试框架 Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring Boot项目 ...

  5. Spock测试套件入门

    目录 Spock测试套件 核心概念 整体认识 前置.后置 同junit的类比 Feature 方法 blocks 典型的用法 异常condition then和expect的区别 cleanup bl ...

  6. Groovy+Spock单元测试

    一.导入依赖 Spock是基于JUnit的单测框架,提供一些更好的语法,结合Groovy语言,可以写出更为简洁的单测. <!-- groovy依赖 --> <dependency&g ...

  7. Compile Groovy/Spock with GMavenPlus

    在之前的博文里曾使用GMaven插件编译Groovy/Spock,这次使用GMavenplus插件,更加方便. 具体步骤 1. 导入Spock和Groovy依赖 <dependency> ...

  8. [每日一学]apache camel|BDD方式开发apache camel|Groovy|Spock

    开发apache camel应用,最好的方式就是tdd,因为camel的每个组件都是相互独立并可测试的. 现在有很多好的测试框架,用groovy的Spock框架的BDD(行为测试驱动)是比较优秀和好用 ...

  9. [编译原理]用BDD方式开发lisp解释器(编译器)|开发语言java|Groovy|Spock

    lisp是一门简单又强大的语言,其语法极其简单: (+ 1 2 ) 上面的意思 是:+是方法或函数,1 ,2 是参数,fn=1+2,即对1,2进行相加求值,结果是:3 双括号用来提醒解释器开始和结束. ...

随机推荐

  1. poj 1185(状态压缩DP)

    poj  1185(状态压缩DP) 题意:在一个N*M的矩阵中,‘H'表示不能放大炮,’P'表示可以放大炮,大炮能攻击到沿横向左右各两格,沿纵向上下各两格,现在要放尽可能多的大炮使得,大炮之间不能相互 ...

  2. Ubuntu16.04中MySQL之中文不能插入问题

    转自:http://blog.csdn.net/fr555wlj/article/details/55668476 今天下午在学习MySQL时,向表中插入一条数据含有中文,结果报错如下, ERROR ...

  3. Java多线程(四) —— 线程并发库之Atomic

    一.从原子操作开始 从相对简单的Atomic入手(java.util.concurrent是基于Queue的并发包,而Queue,很多情况下使用到了Atomic操作,因此首先从这里开始). 很多情况下 ...

  4. There is no getter for property named 'notice' in 'class com.game.domain.Notices'

    在插入数据时报错:There is no getter for property named 'notice' in 'class com.game.domain.Notices' 四月 11, 20 ...

  5. Qt——常用控件样式

    下面是我设计.调整.修改的Qt控件样式,仅供参考. Github地址:https://github.com/ikongziming/QtDemo/tree/master/StyleSheetDemo ...

  6. bzoj 2124 等差子序列 (线段树维护hash)

    2124: 等差子序列 Time Limit: 3 Sec  Memory Limit: 259 MBSubmit: 1922  Solved: 714[Submit][Status][Discuss ...

  7. tarjan解决路径询问问题

    好久没更新了,就更一篇普及组内容好了. 首先我们考虑如何用tarjan离线求出lca,伪代码大致如下: def tarjan(x): 将x标记为已访问 for c in x的孩子: tarjan(c) ...

  8. 【转】vi编辑只读文档无法保存的解决办法

    vi编辑只读文档无法保存的解决办法 使用普通用户编辑nginx.conf 等配置文件: 保存的时 候会提示:没有Root Permission 可以用如下方法解决:保存时加上::w !sudo tee ...

  9. 用Python实现的数据结构与算法:链表

    一.概述 链表(linked list)是一组数据项的集合,其中每个数据项都是一个节点的一部分,每个节点还包含指向下一个节点的链接(参考 <算法:C语言实现>). 根据结构的不同,链表可以 ...

  10. hdu 5852 :Intersection is not allowed! 行列式

    有K个棋子在一个大小为N×N的棋盘.一开始,它们都在棋盘的顶端,它们起始的位置是 (1,a1),(1,a2),...,(1,ak) ,它们的目的地是 (n,b1),(n,b2),...,(n,bk). ...