Spock是一个测试框架,基于BDD(行为驱动开发)思想实现。它结合Groovy动态语言的特点,提供了各种标签,并采用简单、通用、结构化的描述语言,让编写测试代码更加简洁、高效。
常用的测试框架Junit、JMock、mockito各有所长,但是各种框架在开发过程中写出的测试风格各异,难以阅读,使得测试变成了一个门槛越来越高,越来越难以实现的模块。
直观的来说,Spock是一种有标签化属性的一种语言,通过给出的多种标签(given、when、then、where)去描述代码应该实现什么功能。Spock自带Mock功能,加上groovy的语法,可以简单的写出高效的代码,可读性更强。
在IDE中新建一个gradle项目,导入以下依赖:
plugins {
id 'java'
id 'groovy'
}
group 'org.mxb'
version '1.0-SNAPSHOT'
repositories {
maven { url 'https://maven.aliyun.com/repository/jcenter' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/central' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
mavenCentral()
}
dependencies {
// groovy依赖和spock依赖
testImplementation 'org.codehaus.groovy:groovy-all:3.0.6'
testImplementation 'org.spockframework:spock-core:2.0-M4-groovy-3.0'
}
test {
useJUnitPlatform()
}
下面就开始一个第一个简单的示例:
// 继承自Specification
class TestSpock extends Specification{
// 测试名字
def "My Test"() {
// 数据准备
given:
// 示例类,调用getNum可以获取数字
def ng = new NumberGetter(2)
// 调用函数
when:
def num = ng.getNum()
// 结果检查
then:
num == 2
}
}
运行结果如图所示:
可以看出,测试已经通过了。
这里的测试逻辑比较清晰:我有一个NumberGetter类,有一个变量num,可以通过getNum()函数获取里面的数字(本例中通过构造函数初始化为2)。
下面将代码进行修改,使得测试失败:
修改为:
// 结果检查
then:
num == 1
运行结果如下:
这里可以方便的看出哪一个表达式出了问题,也可以看出变量的实际值是什么。
Spock使用了以下几个标签,分别是:
标签 | 含义 |
---|---|
given/setup | 用于数据准备 |
when | 和then一起使用,可以包含任意代码,一般用于模拟 |
then | 和then一起使用,仅限于条件、异常条件、交互和变量定义 |
cleanup | 用于释放特征方法使用的任何资源,前面抛出异常也会正常执行 |
where | 出现在方法后面,且只能存在一个where块,用于编写数据驱动的特征方法 |
expect | 期望块,可以理解为用于简化的when-then |
刚刚已经有了given-when-then的示例,下面是expect、cleanup、where的示例。
expect可以简化when-then逻辑:
when:
def x = Math.max(1, 2)
then:
x == 2
//等价于:
expect:
Math.max(1, 2) == 2
根据官网建议:when-then描述具有副作用的方法,expect描述纯函数方法。
cleanup标签类似于try-catch-finally块中的finally,用于最后的处理:
setup:
def file = new File("/some/path")
file.createNewFile()
// ...
cleanup:
file.delete()
where用于用于编写数据驱动的特征方法,例如如果需要输入多组数据,可以无需重新写用例,而是采用where
def "computing the maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a << [5, 3]
b << [1, 9]
c << [5, 9]
}
此外,with也是一个常用的关键字,一般用于验证结果,消除冗长的验证语句:
with(response) {
status == "success"
id == 1
name == "张三"
}
在Spring boot的项目中,需要测试一个接口,传入一个学生的id,需要返回学生的名字,接口为:
public interface FindStudentInfoService{
/**
* 获取学生信息
*
* @param studentId 要查询的学生id
* @return result
*/
String findStudentInfo(long studentId)
}
此接口实现逻辑中,我们需要对查询用户做一个权限检查,查询这个用户是不是有权限进行此次查询。
例如,检查调用者是不是老师或者学生管理人员,否则不能检查。需要调用其他团队写好的RamCheckService接口进行查询:
public interface RamCheckService{
/**
* Ram鉴权
*
* @param param 此处传入调用者的相关信息参数
* @return result
*/
RamCheckResponse ramCheck(RamCheckModel param);
}
此处传入调用者的相关信息,返回一个RamCheckResponse,包括一个字符串的权限描述,如"Teacher",代表此人具有teacher权限:
class RamCheckResponse{
// 例如"Teacher"代表具有教师权限,"Admin"代表具有管理员权限
String authority;
}
那么,我们的这个测试可以通过如下方式构建:
// 测试对象
@Title("测试接口:FindStudentInfoService#findStudentInfo")
@Subject(FindStudentInfoService)
class FindStudentInfoServiceSpec extends Specification{
// 要测试的接口,采用Resource是获取真实的容器管理的bean
@Resource
private FindStudentInfoService findStudentInfoService
@SpringSpy
private RamCheckService ramCheckService
def "test Find Student Info sucess"()
{
// 准备测试数据
given:
long testId = 1L
// 调用要测试的接口
when:
def response = findStudentInfoService.findStudentInfo(testId)
// 此处模拟了ram检查
// 1*代表模拟一次此接口的调用
// ramCheck(_)中,_ 表示参数可以为任意的内容,后面会详细介绍
then: '模拟一个Ram检查'
1 * ramCheckService.ramCheck(_) >> new RamCheckResponse(authority : "Teachr")
then: '检查接口返回值'
assert response == "张三"
}
这样就进行了一次简单的测试,主要实现了测试findStudentInfoService下的findStudentInfo()接口,模拟出了一个权限检查接口的返回值(例如是其他团队写的rpc接口),并最终检查结果是不是法外狂徒张三。
需要详细解释一些这里的模拟调用接口RamCheckService的代码:
1 * 代表模拟一次,例如此处调用了两次检查可以写为 2 * , 若不关心次数可以写为**_ ***
>> 代表了模拟的调用此接口的返回值
ramCheck(_)的参数 _ 代表此处可以匹配任意参数,如果存在重载的函数,则也可以指定参数类型,如:ramCheck(RamCheckModel)
如果需要进行异常测试的相关逻辑,则可以使用where块进行测试。例子改编自于Spock代码讲解-异常测试
某函数来解析用户参数是否正确,逻辑如下:
/**
* 校验请求参数user是否合法
* @param user
* @throws APIException
*/
public void validateUser(UserVO user) throws APIException {
if(user == null){
throw new APIException("10001", "user is null");
}
if(null == user.getName() || "".equals(user.getName())){
throw new APIException("10002", "user name is null");
}
if(user.getAge() == 0){
throw new APIException("10003", "user age is null");
}
if(null == user.getTelephone() || "".equals(user.getTelephone())){
throw new PhoneException("10004", "user telephone is null");
}
}
APIException和PhoneException是封装的业务异常:
/**
* 自定义业务异常
*/
public class APIException extends RuntimeException {
private String errorCode;
private String errorMessage;
setXXX...
getXXX...
}
那么在测试的时候可以这样写:
class UserControllerTest extends Specification {
def userController = new UserController()
def "验证用户信息的合法性: #expectedMessage"() {
when: "调用校验用户方法"
userController.validateUser(user)
then: "捕获异常并设置需要验证的异常值"
def exception = thrown(exception)
where: "表格方式验证用户信息的合法性"
index | description | user |exception
1 | "name为null" | new UserVO(age : 12, telephone : "1234556") | APIException
2 | "age为null" | new UserVO(name : "张三", telephone : "1234556") | APIException
3 | "phone为null" | new UserVO(age : 12, name : "张三") | PhoneException
}
官方文档
Spock系列
吃透单元测试:Spock单元测试框架的应用与实践