Spock 测试框架基于 Groovy 并吸收了 Junit、TestNG、Mockito 等测试框架的优点。 Spock 编写的单元测试层次清晰,代码量少,可读性好,Groovy 最终会编译为 class 文件,支持各种集成开发环境(eclipse,Intellij Ieda), 尤其是 Intellij idea 已经集成支持 Groovy 的插件,也支持 maven-surefire-plugin、jacoco 等 maven 插件。
Spock 官网,必读书籍《Java Testing with Spock》, 如要速成只需要阅读以下两篇文章
Spock 实例
大多数遵循 TDD 的 Java 开发者均会使用 mockito 或 powermock,但 mockito 和 powermock 均包含了许多样本代码,导致测试代码变得冗长而难以维护。 在测试中引入 Groovy/Spock 后,我完全被它们吸引,并转向使用 Groovy/Spock 来替代原有的测试框架。
定义 Domain,DAO,Service
下面将围绕一个简单例子来讲解 Groovy/Spock,例子中将包含一个 service 类,负责处理 domain 对象,以及一个数据访问层。
public class User {
private int id;
private String name;
private int age;
// Accessors omitted
}
public interface UserDao {
public User get(int id);
}
public class UserService {
private UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public User findUser(int id){
return userDao.get(id);;
}
}
采用 Groovy/Spock 针对 UserService 编写测试
class UserTest extends Specification {
UserDao dao = Mock(UserDao)
UserService service = new UserService(dao)
def "it gets a user by id"() {
given:
1 * dao.get(id) >> new User(id: id, name: name, age: age)
when:
def result = service.findUser(id)
then:
result.id == userId
result.name == userName
result.age == userAge
where:
id | name | age || userId | userName | userAge
1 | "zhang" | 18 || 1 | "zhang" | 18
2 | "charles" | 28 || 2 | "charles" | 28
}
}
在 Spock 中创建 mock 对象非常容易,只需要使用 Mock(Class)这样的语句即可。如上所述,mock 后的 DAO 对象被传入 userService 中。 Setup 方法会在每个测试方法运行前被执行。
Spock 是一个 BDD 测试框架,因此对于 Spock 中涉及的 given,when,then 样式最简单的理解就是:
- given 给定一些条件
- when 当执行一些操作时
- then 期望得到某个结果
- where 多套测试数据的检测和验证
分块 替换 功能 限制 given setup 初始化函数,mock 非必要 when expect 执行待测试的函数 when 和 then 必须对成出现 then expect 验证函数结果 when 和 then 可以被 expect 替换 where 多套测试数据的检测 spock 的特性功能 and 对其余块进行分隔说明 非必要
如上述测试方法中 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()));
}
}
结合 PowerMock mock 静态方法
@RunWith(PowerMockRunner)
@PowerMockRunnerDelegate(Sputnik)
@PowerMockIgnore({
"javax.net.ssl.*",
"javax.management.*",
"com.sun.crypto.*",
"javax.crypto.*"})
@PrepareForTest([BizEngine.class])
class BaseSimpleTest extends Specification {
def "test engine"() {
setup:
BizEngine mockStatic = PowerMockito.mock(BizEngine.class)
Whitebox.setInternalState(BizEngine.class, "bizEngine", mockStatic)
when:
Mockito.when(mockStatic.getPoint(Point.class, null, null)).thenReturn(new LocationPoint())
then:
println "test engine end"
}
}
其他提示
- 最重要也是最容易被遗忘的提示,阅读 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