Spock 基于BDD测试

Spock 测试框架基于 Groovy 并吸收了 Junit、TestNG、Mockito 等测试框架的优点。 Spock 编写的单元测试层次清晰,代码量少,可读性好,Groovy 最终会编译为 class 文件,支持各种集成开发环境(eclipse,Intellij Ieda), 尤其是 Intellij idea 已经集成支持 Groovy 的插件,也支持 maven-surefire-plugin、jacoco 等 maven 插件。

image.png
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
https://alicharles.oss-cn-hangzhou.aliyuncs.com/static/images/mp_qrcode.jpg
文章目录
  1. Spock 实例
    1. 定义 Domain,DAO,Service
    2. 采用 Groovy/Spock 针对 UserService 编写测试
    3. 结合 PowerMock mock 静态方法
  2. 其他提示