Spock测试框架

介绍

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单元测试框架的应用与实践

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值