目录
多变量的数据管道(Multi-Variable Data Pipes)
引入maven依赖
<!-- spock -->
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.0-groovy-2.4</version>
<scope>test</scope>
</dependency>
项目实战
package com.youzan.ebiz.salesman.wap.biz.groovy
import com.youzan.ebiz.mall.commons.helper.ResultHelper
import com.youzan.ebiz.salesman.api.push.NsqPushService
import com.youzan.ebiz.salesman.api.relation.RelationApiService
import com.youzan.ebiz.salesman.api.shoppingguide.ShoppingGuideCheckApiService
import com.youzan.ebiz.salesman.wap.api.dto.relation.CustomerRelationSaveWapDTO
import com.youzan.ebiz.salesman.wap.api.dto.resp.StatusDTO
import com.youzan.ebiz.salesman.wap.biz.api.customer.CustomerWapApiServiceImpl
import com.youzan.ebiz.salesman.wap.biz.service.WhitelistService
import com.youzan.ebiz.salesman.wap.biz.service.dependency.shoppingguide.GuideFrontRelationWrapperService
import com.youzan.ebiz.salesman.wap.biz.service.dependency.shoppingguide.ShoppingGuideReadApiWrapperService
import com.youzan.guide.api.service.relation.CustomerRelationWriteApiService
import spock.lang.Specification
import spock.lang.Unroll
/**
* @author zhangkun* @date 2022/1/18 下午7:10
* @version 1.0
*/
class CustomerWapApiServiceImplTest extends Specification {
def customerWapApiServiceImpl = new CustomerWapApiServiceImpl();
def nsqPushService = Mock(NsqPushService);
def whitelistService = Mock(WhitelistService);
def guideFrontRelationWrapperService = Mock(GuideFrontRelationWrapperService);
def relationApiService = Mock(RelationApiService);
def customerRelationWriteApiService = Mock(CustomerRelationWriteApiService);
def shoppingGuideCheckApiService = Mock(ShoppingGuideCheckApiService)
def shoppingGuideReadApiWrapperService = Mock(ShoppingGuideReadApiWrapperService)
def setup() {
customerWapApiServiceImpl.nsqPushService = nsqPushService
customerWapApiServiceImpl.whitelistService = whitelistService
customerWapApiServiceImpl.guideFrontRelationWrapperService = guideFrontRelationWrapperService
customerWapApiServiceImpl.relationApiService = relationApiService
customerWapApiServiceImpl.customerRelationWriteApiService = customerRelationWriteApiService
customerWapApiServiceImpl.shoppingGuideCheckApiService = shoppingGuideCheckApiService
customerWapApiServiceImpl.shoppingGuideReadApiWrapperService = shoppingGuideReadApiWrapperService
}
@Unroll
def "testBindCustomerRelation #caseName"() {
given: "准备数据"
def customerRelationSaveWapDTO = new CustomerRelationSaveWapDTO(bindSourceType: bindSourceType, kdtId: kdtId, sellerFrom: sellerFrom, buyerId: buyerId, shoppingGuideBindSourceType: shoppingGuideBindSourceType);
and: "mock数据"
shoppingGuideCheckApiService.isNormalShoppingGuide(_) >> isShoppingGuide;
shoppingGuideReadApiWrapperService.getShoppingGuideByUserId(_, _) >> isPosShoppingGuide
nsqPushService.pushNsq(_) >> null;
whitelistService.inWhiteList(_, _) >> inWhiteList;
guideFrontRelationWrapperService.bindCustomerRelationForWap(_) >> true;
when: "调用接口"
print("入参:--->" + customerRelationSaveWapDTO);
def res = customerWapApiServiceImpl.bindCustomerRelation(customerRelationSaveWapDTO);
print("返回结果--->" + res)
then: "校验结果"
code == res.code;
data == res.data;
message == res.message;
where:
caseName | isShoppingGuide | isPosShoppingGuide | inWhiteList | userId | shoppingGuideBindSourceType | bindSourceType | kdtId | buyerId | fansId | fansType | sellerFrom | code | data | message
"userId不存在" | null | null | false | 1 | 1 | 1 | 1 | 1 | 1 | 1 | "sl" | 300000000 | null | "查询结果为空"
"sl为空" | null | null | false | 1 | 1 | 1 | 1 | 1 | 1 | 1 | null | 200 | new StatusDTO(true) | "successful"
"sl为undefined" | null | null | false | 1 | 1 | 1 | 1 | 1 | 1 | 1 | "undefined" | 200 | new StatusDTO(true) | "successful"
"sl错误" | null | null | false | 1 | 1 | 1 | 1 | 1 | 1 | 1 | "sajjv" | 300000000 | null | "查询结果为空"
"切流新方法" | ResultHelper.success(true) | null | true | 1 | 1 | 1 | 1 | 1 | 1 | 1 | "sajjv" | 200 | new StatusDTO(true) | "successful"
"未切流新方法" | ResultHelper.success(true) | null | false | 1 | 1 | 1 | 1 | 1 | 1 | 1 | "sajjv" | 200 | new StatusDTO(false) | "successful"
}
}
下面再来仔细介绍下spock框架
Spock测试框架
一、为什么要使用Spock
用JUnit写的单元测试
@Test
public void addPerson() {
// 正常添加
PersonVo personVo = PersonVo.builder()
.idCardNo("1345")
.name("Jack")
.sex("male")
.build();
Assert.assertTrue(this.personService.addPerson(personVo));
// 名字重复
personVo = PersonVo.builder()
.idCardNo("1346")
.name("Jack")
.sex("male")
.build();
Assert.assertFalse(this.personService.addPerson(personVo));
// idCardNo重复
personVo = PersonVo.builder()
.idCardNo("1345")
.name("Jack Chen")
.sex("male")
.build();
Assert.assertFalse(this.personService.addPerson(personVo));
}
使用Spock编写同样的单元测试
@Unroll
def "addPerson:(idCardNo->#idCardNo, sex->#sex, name->#name), expect:#result"() {
// 前置条件 同setup
given:
def personVo = PersonVo(
idCardNo: idCardNo,
name: name,
sex: sex
)
// 预期
expect:
result == this.personService.addPerson(personVo)
// 条件
where:
// 数据定义方法一
// |用来分隔输入 ||用来分隔输出
idCardNo | name | sex || result
"5101" | "Jack" | "male" || true
// idCardNo重复
"5101" | "John" | "male" || false
// name重复
"5102" | "Jack" | "male" || false
"123456" | "Lucy" | "female" || true
}
二、Spock基本概念
Specification:
测试类都必须继承Specification类
Fixture Methods
// 每个spec前置
def setupSpec() {
}
// 每个spec后置
def cleanupSpec() {
}
// 每个方法前置
def setup() {
}
// 每个方法后置
def cleanup() {
}
Feature methods
// 动态方法名
@Unroll
def "addPerson:(idCardNo->#idCardNo, sex->#sex, name->#name), expect:#result"() {
}
// 固定方法名
def addPerson(){
}
setup/given Blocks
在这个block中会放置与这个测试函数相关的初始化程序
given: // 也可以写作setup
def stack = new Stack()
def elem = "push me"
when and then Blocks
when:
stack.push(elem)
then:
!stack.empty
stack.size() == 1
stack.peek() == elem
expect Blocks
when and then Blocks例子可以替换为:
given:
def stack = new Stack()
def elem = "push me"
stack.push(elem)
expect:
stack.empty == false
stack.size() == 1
stack.peek() == elem
where Blocks
做测试时最复杂的事情之一就是准备测试数据,尤其是要测试边界条件、测试异常分支等,这些都需要在测试之前规划好数据.
def "maximum of two numbers"() {
expect:
// exercise math method for a few different inputs
Math.max(1, 3) == 3
Math.max(7, 4) == 7
Math.max(0, 0) == 0
}
// 可以替换为
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a | b || c
3 | 5 || 5
7 | 0 || 7
0 | 0 || 0
}
三、Spock基本使用
class CalculateSpec extends Specification {
// 每个spec前置
def setupSpec() {
calculateService = new CalculateService()
println ">>>>>> setupSpec"
}
// 每个方法前置
def setup() {
println ">>>>>> setup"
}
// 每个方法后置
def cleanup() {
println ">>>>>> cleanup"
}
// 每个spec后置
def cleanupSpec() {
println ">>>>>> cleanupSpec"
}
def "test life cycle"() {
given:
def a = 1
def b = 2
expect:
a < b
println "test method finished!"
}
}
基本标签语句组合形
- given … expect …
- given … when … then …
- when … then …
- given … expect … where …
- expect … where …
- expect
with() 和 verifyAll()
def "test person use with(p)"() {
given: "init a person"
Date now = new Date()
Person p = new Person(name: "yawn", age: 18, birthday: now)
expect: "测试p"
with(p) {
name == "yawn"
age < 20
birthday == now
}
}
spock异常处理
@Unroll
def "exception handle"() {
when:
ValidateUitls.validate(new SomeDTO(id: id, name: name))
then:
def exception = thrown(Exception)
errMsg == exception.getMessage
where:
id | name | errMsg
null | null | "some error message"
}
spring环境中使用spock
@SpringBootTest
@ContextConfiguration
class SpringBootSpec extends Specification {
@Share
CalculateService calculateService;
def "spring boot test"() {
expect: "asas"
z == calculateService.minus(x, y)
where:
x << [9, 8, 7]
y << [6, 5, 4]
z << [3, 3, 3]
}
def "spring boot test2"() {
expect: "asas"
z == calculateService.minus(x, y)
where:
x | y | z
9 | 8 | 1
6 | 5 | 1
3 | 3 | 0
}
}
四、Spock数据驱动
Spock数据驱动测试( Data Driven Testing ),就是测试用例的书写格式更加面向数据,spock的数据驱动测试的书写格式,可即很清晰地汇集大量测试数据。
其中测试方法的参数 int a, int b, int c
称为数据变量(data variables),where 标签后的语句块称为数据表(data table)。
class MathSpec extends Specification {
def "maximum of two numbers"(int a, int b, int c) {
expect:
Math.max(a, b) == c
where:
a | b | c
1 | 3 | 3
7 | 4 | 7
0 | 0 | 0
}
}
数据表(data table)
数据表(data table)就是一个看起来直观的存放测试数据的表格。数据表的第一行表头对应数据变量,从第二行开始就是数据行(data rows)。
如果测试方法只有一个数据变量a,则需要
|
数据表的简化写法
由于数据变量在被测试的语句Math.max(a, b) == c
中有所体现,所以测试方法的参数可以省略掉;此外,被测试的方法Math.max(a, b) == c
中,a,b为参数,c
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a | b || c
1 | 3 || 3
7 | 4 || 7
0 | 0 || 0
}
where
模块第一行代码是表格的列名,多个列使用|
单竖线隔开,||
双竖线区分输入和输出变量,即左边是输入值,右边是输出值。格式如下:
输入参数a | 输入参数b || 输出结果c
数据管道(Data Pipes)
其实,上面介绍的数据表只是数据管道的语法糖。数据管道(data pipes)就是给每一个数据变量提供一组值,写法如下:
|
一个数据管道,由两部分组成,<<
前面的是数据变量,<<
后面的是数据来源( data provider )。其中 数据来源( data provider ) 可以为任何Groovy中可遍历的对象,包括 Collection
, String
, Iterable
等类型的对象和实现了 Iterable
的对象。
数据来源( data provider )也不要求是一定是类似的集合,它也可以是从外部的文本文件、数据库、电子表格等获取数据的代码,甚至是自动生成数据的代码片段。
多变量的数据管道(Multi-Variable Data Pipes)
如果一个数据来源(data provider)每次迭代可以提供多个变量,则可以将它做为多个数据变量的数据来源 ,形成一个多变量的数据管道(Multi-Variable Data Pipes)。如下:
@Shared sql = Sql.newInstance("jdbc:mysql://localhost:3306/test", "com.mysql.jdbc.Driver", "root", "root")
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
[a, b, c] << sql.rows("select a, b, c from maxdata")
}
如果数据来源的某列返回值我们并不使用,则可以使用下划线“_”,这样就会被忽略,如下:
|
五、spock测试桩mock和stub的使用
如图,有如上的方法调用关系(模块依赖关系):A调用B和E方法,B调用C和D方法。在使用spock进行单元测试时,有如下情景,分别可使用stub和mock。
使用stub测试桩
如果我们需要测试A方法,但是E方法目前还没办法调用,或者还没开发完成。这种场景下,就可以使用stub测试桩。stub测试桩可以给E方法模拟一个或多个假的返回值,我们测试时只需要调用stub对象的E方法即可,调用后的返回值是我们在生成stub对象时指定的。如下:
def "Stub 测试桩"() {
given: "构造测试桩"
CalculateInterface calculateService = Stub(CalculateInterface)
calculateService.plusPlus(_) >> 1
when:
int x = calculateService.plusPlus(12)
int y = calculateService.plusPlus(3)
then:
x == 1
y == 1
}
上面代码中,calculateService.plusPlus(_) >> 1
给一个并未实现的plusPlus()方法指定了返回值为1,测试代码就可以直接调用这个方法了。
其中这个语句的常用格式有:
|
生成返回值
|
通过计算生成返回值
这种方式,生成返回值的格式时一个闭包
|
如果想调用方法抛出异常
|
链式生成返回值
|
上面代码中,方法被调用的前三次分别返回 “ok”, “fail”, “ok”,第四次会抛出异常,第五次及以后调用,会返回“ok”。
以上是spock中stub测试桩的使用场景,总结为一句就是: stub测试桩给被调用者( 方法/模块)制造假的返回值,以便不影响调用者的测试。
使用mock测试桩
mock测试桩就是模拟一个测试的结果。如下图,A类调用类B和C类的某个方法:
如果要测试A的方法,但是我们没办法调用B来检测结果,就可以使用mock测试桩,生成一个B的mock对象。检验结果时,可以使用B的mock对象替代B。这个结果一般是B和C方法的调用或者状态的改变。
def subscriber = Mock(Subscriber) // 1. 创建一个mock对象
def "should send messages subscriber"() {
when:
publisher.send("hello") // 2. publisher 发送一个“hello”
then:
1 * subscriber.receive("hello") // 3. subscriber 接收到一个“hello”
1 * subscriber.messageCount == 1
}
mock和stub测试桩的对比
- mock测试桩用于检测结果。
- stub测试桩用于提供测试的条件。
六、Spock注解使用
@Share
在测试类中,Share标记的变量可以在不同的测试方法中使用。
@Ignore 忽略
- 忽略测试方法
@IgnoreRest 忽略其他
- 忽略其他测试方法
@Unroll 展开数据管道的测试用例
- 展开:数据驱动测试中,展开所有的测试结果,分别显示每个测试用例的测试情况
@FailsWith(ArithmeticException.class) 标记失败
- 记录已经知道的 bug
- 标记让方法执行失败的测试用例
@Timeout(value = 10, unit = TimeUnit.MILLISECONDS) 超时时间设置
- 超时就失败
@IgnoreIf 根据条件忽略
|
@Requires 根据条件执行
|
@Retry 重试
|
@Unroll 展开
七、实战案例
1、数据驱动
2、stub测试桩
|
八、注意事项
1、新增ut