Spock测试框架基于Groovy并吸收了Junit、TestNG、Mockito等测试框架的优点。
Spock编写的单元测试层次清晰,代码量少,可读性好。
Groovy无缝兼容Java:Groovy最终会编译为class文件,JVM并不在乎class来自Java还是Groovy文件,支持各种集成开发环境(eclipse,Intellij Ieda),尤其是Intellij idea已经集成支持Groovy的插件,也支持maven-surefire-plugin、jacoco等maven插件。
1.学习前的准备
官网:http://spockframework.org
必读书籍:《Java Testing with Spock》
如要速成只需要阅读以下两篇文章:
5分钟入门Groovy: https://learnxinyminutes.com/docs/groovy/
一篇非常详尽的介绍Spock的英文教程:https://semaphoreci.com/community/tutorials/stubbing-and-mocking-in-java-with-the-spock-testing-framework
2.Maven依赖
Eclipse支持Groovy需要安装插件:https://github.com/groovy/groovy-eclipse/wiki
注意Maven作用域控制在test
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.1-groovy-2.4</version>
<scope>test</scope>
</dependency>
<dependency> <!-- enables mocking of classes (in addition to interfaces) -->
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.6.5</version>
<scope>test</scope>
</dependency>
<dependency> <!-- enables mocking of classes without default constructor (together with
CGLIB) -->
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>2.5.1</version>
<scope>test</scope>
</dependency>
3. 被测试的类
//数据库映射类
public class DbEntity {
long id;
String requestNo;
//省略get set
}
//被测试的service
@Component
public class MyService {
@Autowired
private DbEntityDao dbEntityDao;
private String value;
public String getValue() {
return value;
}
public int stringToInteger(String valueStr) {
return Integer.valueOf(valueStr);
}
public String getUtilValue(String id) {
return id+MyServiceUtil.getValue(id);
}
public void setValue(String value) {
this.value = value;
}
public long selectByRequestNo(String requestNo){
DbEntity dbEntity = dbEntityDao.selectByRequestNo(requestNo);
return dbEntity.getId();
}
public int add(int a,int b){
return a+b;
}
}
//数据库操作类
public interface DbEntityDao {
DbEntity selectByRequestNo(@Param("requestNo")String requestNo);
}
//工具类
public class MyServiceUtil {
public static String getValue(String value){
return "MyServiceUtil";
}
}
4. given-when-then声明
given-when-then是Spock的基本句式,使单元测试层次清晰。
此外还有and关键字,用于将大段的声明分割开来。
所有的Spock单元测试都继承spock.lang.Specification,Specification基于Groovy dsl提供了测试环境。
Spock支持将一个句子作为方法名,实现自解释。
Stub用来创建要模拟的测对象。
“>>”用来表示模拟对象的返回值
“>>>”用来表示同一方法多次按顺序调用返回不同值
下划线“_”表示匹配所有的输入值。
Spock不使用Assert来校验结果,then声明后面的表达式result == “1”就相当于Junit中的Assert.assertTrue(result.equals(“1”))
下面通过例子展示以下用法,Groovy可以不写分号
import spock.lang.Specification
/**
* JUnit 的测试用例总是由 Runner 去执行
* Specification的注解:@RunWith(Sputnik.class) 是对 org.junit.runner.Runner的扩展
*/
class MyServiceTest extends Specification {
//groovy里面用def定义所有的对象,包含方法声明
def mockRequestNo = "123"
def "最基本的测试:mock返回值"() {
given: "given用来准备mock对象,可以放到when里面"
and:"and声明可选"
myService.getValue() >> "1"
when: "when里面调用被测试的方法"
String result = myService.getValue()
then: "then用来验证结果"
result == "1"
}
def "最基本的测试:多次调用返回不同值"() {
given: "返回多个值使用三个>"
MyService myService = Stub(MyService)
myService.getValue() >>> ["1", "2", "3"]
//except相当于when then的合并
expect:
myService.getValue() == "1"
myService.getValue() == "2"
myService.getValue() == "3"
}
def "测试模糊匹配"() {
given:
MyService myService = Stub(MyService)
//下划线表示匹配所有输入值
myService.stringToInteger(_) >> 999
//except相当于when then的合并
expect: "有时候我们并不在乎输入值"
myService.stringToInteger("1") == 999
myService.stringToInteger("2") == 999
myService.stringToInteger("3") == 999
}
}
5. 校验模拟对象的行为
Stub只能模拟对象的返回值,而Mock更进一步,不仅能模拟对象,还能校验对象的调用次数等行为。
表达式: N * mockedObject.method(arguments)>>value,表示参数为arguments的method方法调用N次,返回值是value。该表达式一般放在then后面。
注意,尽管then声明放在when后面,由于基于Groovy AST语法解析树,Spock会先解析该表达式,然后在when后面的测试类执行后再进入then后面的校验逻辑。
Groovy里面一切field都为public,免去了注入的烦恼
Groovy的lambda表达式比Java 8更加灵活,通过lambda可以抓取测试对象的输入值,嵌套很深的测试类很有必要验证一下输入值是否正确。
def mockRequestNo = "123"
def "校验mock对象的调用次数"() {
given:
DbEntity expectEntity = new DbEntity()
//groovy的with语法,简化创建对象
expectEntity.with {
id = 456
requestNo = mockRequestNo
}
//mock跟Stub不同的是可以校验mock对象的调用次数
DbEntityDao dao = Mock(DbEntityDao)
MyService myService = new MyService()
//Groovy里面所有的field都是public的,可以直接访问
myService.dbEntityDao = dao
when:
long id = myService.selectByRequestNo(mockRequestNo)
then:
id == 456
//then除了验证结果,还可以设置mock对象返回值,
//Spock会先解析then中定义的mock,等when执行后再校验mock行为
1 * dao.selectByRequestNo(mockRequestNo) >> expectEntity
}
def "抓取输入值"(){
given:
def resultCapture = null
DbEntityDao dao = Stub(DbEntityDao.class)
//定义lambda表达式,抓取输入值
dao.selectByRequestNo({v-> resultCapture = v })>>null
when:
dao.selectByRequestNo("123")
then:
resultCapture == "123"
}
6. 参数化测试
Spock的参数化测试比Junit更加简洁。
直接使用表格形式来定义输入值跟期望值。
注意输入的参数名必须跟被测试方法参数名一致。
如下代码,表格中的输入参数跟测试方法输入参数名均为a,b
def "test Parameterized"() {
when:
MyService myService = new MyService()
then:
myService.add(a, b) == result
where: "准备参数,输入参数名必须跟方法里面的参数名一致"
a | b || result
1 | 1 || 2
1 | 2 || 3
2 | 2 || 4
}
7. 同其他测试框架混搭
对于static、private方法,Spock还是无能为力。这时候可以结合PowerMock框架。
JUnit 的测试用例总是由 Runner 去执行,JUnit 提供了 @RunWith 注解来指定自定义的 Runner。
如果未指定特别的 Runner,那么会采用默认的 Runner
由于Spock的Runner直接继承自Junit Runner可以很好的扩展。下面展示如何跟PowerMock集成。
import spock.lang.Specification
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([MyServiceUtil.class])
class MyServiceWithOtherRunner extends Specification{
def id = "1024"
def "测试难缠的static方法"(){
given:
PowerMockito.mockStatic(MyServiceUtil.class)
PowerMockito.when(MyServiceUtil.getValue(id)).thenReturn("horrible")
MyService myService = new MyService();
when:
String result = myService.getUtilValue(id)
then:
result == id+"horrible"
}
}
8.其他
该小节展示一些杂项
类似于Junit注解的声明
def setupSpec() { //整个单元测试启动的时候只执行一次,类似Junit的@BeforeClass
println "Will run only once"
}
//每个方法执行前都会执行,等同Junit @Before
def setup() {
println "Will run before EACH feature"
}
def cleanup() { //执行后每次执行
println "Will run once after EACH feature"
}
def cleanupSpec() { //整个单元测试结束后执行
println "Will run once at the end"
}
模拟异常
def "test trade"() {
given:
mockObject.method() >> { throw new RuntimeException("哈哈,中计了") }
when: "踩坑"
mockObject.method()
then: "validator"
RuntimeException runtimeException = thrown(RuntimeException)
runtimeException.message == "哈哈,中计了"
}
简化校验
then:
mockObject.value1 == 1
mockObject.value2 == 2
mockObject.value3 == 3
mockObject.value4 == 4
得益于Groovy with闭包,还可以简化成这样
then:
with(mockObject){
value1 == 1
value2 == 2
value3 == 3
value4 == 4
}
同样使用Stub初始化对象也可以写成这样,isEmpty()等均是类WarehouseInventory里面的方法。
when:
WarehouseInventory inventory = Stub(WarehouseInventory) {
isProductAvailable("bravia",1) >> true
isProductAvailable("panasonic",1) >> false
isEmpty() >> false
}