Spock
# 资源
- 示例:https://github.com/spockframework/spock-example
- 官方文档:https://spockframework.org/spock/docs/2.0/index.html
- (推荐)美团经验文档:https://tech.meituan.com/2021/08/06/spock-practice-in-meituan.html
class MyFirstSpecification extends Specification {
// fields
// fixture methods
// feature methods
// helper methods
}
#共享方法,可以被多个功能方法使用
@Shared res = new VeryExpensiveResource()
def setupSpec() {} // runs once - before the first feature method
def setup() {} // runs before every feature method
def cleanup() {} // runs after every feature method
def cleanupSpec() {} // runs once - after the last feature method
// where
// where模块第一行代码是表格的列名,多个列使用|单竖线隔开,||双竖线区分输入和输出变量,即左边是输入值,右边是输出值。格式如下:
// 输入参数1 | 输入参数2 || 输出结果1 | 输出结果2
// 即把请求参数值和返回结果值的字符串动态替换掉,#id、#postCodeResult、#abbreviationResult#号后面的变量是在方法内部定义的,实现占位符的功能。
// @Unroll注解,可以把每一次调用作为一个单独的测试用例运行,这样运行后的单元测试结果更加直观:
@Unroll
def "input 学生id:#id, 返回的邮编:#postCodeResult, 返回的省份简称:#abbreviationResult"() {
given: "Mock返回的学生信息"
studentDao.getStudentInfo() >> students
when: "获取学生信息"
def response = tester.getStudentById(id)
then: "验证返回结果"
with(response) {
postCode == postCodeResult
abbreviation == abbreviationResult
}
where: "经典之处:表格方式验证学生信息的分支场景"
id | students || postCodeResult | abbreviationResult
1 | getStudent(1, "张三", "北京") || "100000" | "京"
2 | getStudent(2, "李四", "上海") || "200000" | "沪"
}
def getStudent(def id, def name, def province) {
return [new StudentDTO(id: id, name: name, province: province)]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
异常处理
@Unroll
def "validate student info: #expectedMessage"() {
when: "校验"
tester.validateStudent(student)
then: "验证"
def exception = thrown(expectedException)
exception.code == expectedCode
exception.message == expectedMessage
where: "测试数据"
student || expectedException | expectedCode | expectedMessage
getStudent(10001) || BusinessException | "10001" | "student is null"
getStudent(10002) || BusinessException | "10002" | "student name is null"
getStudent(10003) || BusinessException | "10003" | "student age is null"
getStudent(10004) || BusinessException | "10004" | "student telephone is null"
getStudent(10005) || BusinessException | "10005" | "student sex is null"
}
def getStudent(code) {
def student = new StudentVO()
def condition1 = {
student.name = "张三"
}
def condition2 = {
student.age = 20
}
def condition3 = {
student.telephone = "12345678901"
}
def condition4 = {
student.sex = "男"
}
switch (code) {
case 10001:
student = null
break
case 10002:
student = new StudentVO()
break
case 10003:
condition1()
break
case 10004:
condition1()
condition2()
break
case 10005:
condition1()
condition2()
condition3()
break
}
return student
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
Mock
class StudentServiceSpec extends Specification {
def studentDao = Mock(StudentDao)
def tester = new StudentService(studentDao: studentDao)
def "test getStudentById"() {
given: "设置请求参数"
def student1 = new StudentDTO(id: 1, name: "张三", province: "北京")
def student2 = new StudentDTO(id: 2, name: "李四", province: "上海")
and: "mock studentDao返回值"
studentDao.getStudentInfo() >> [student1, student2]
when: "获取学生信息"
def response = tester.getStudentById(1)
then: "结果验证"
with(response) {
id == 1
abbreviation == "京"
postCode == "100000"
}
}
}
// def studentDao = Mock(StudentDao) 这一行代码使用Spock自带的Mock方法,构造一个studentDao的Mock对象,如果要模拟studentDao方法的返回,只需studentDao.方法名() >> "模拟值"的方式,两个右箭头的方式即可
// _ 表示匹配任意类型参数
List<StudentDTO> students = studentDao.getStudentInfo(_);
// 如果有同名的方法,使用as指定参数类型区分
List<StudentDTO> students = studentDao.getStudentInfo(_ as String);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
静态方法Mock
使用 PowerMockito 与 Spock 结合。
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([AbbreviationProvinceUtil.class])
@SuppressStaticInitializationFor(["example.com.AbbreviationProvinceUtil"])
class StudentServiceStaticSpec extends Specification {
def studentDao = Mock(StudentDao)
def tester = new StudentService(studentDao: studentDao)
void setup() {
// mock静态类
PowerMockito.mockStatic(AbbreviationProvinceUtil.class)
}
def "test getStudentByIdStatic"() {
given: "创建对象"
def student1 = new StudentDTO(id: 1, name: "张三", province: "北京")
def student2 = new StudentDTO(id: 2, name: "李四", province: "上海")
and: "Mock掉接口返回的学生信息"
studentDao.getStudentInfo() >> [student1, student2]
and: "Mock静态方法返回值"
PowerMockito.when(AbbreviationProvinceUtil.convert2Abbreviation(Mockito.any())).thenReturn(abbreviationResult)
when: "调用获取学生信息方法"
def response = tester.getStudentByIdStatic(id)
then: "验证返回结果是否符合预期值"
with(response) {
abbreviation == abbreviationResult
}
where:
id || abbreviationResult
1 || "京"
2 || "沪"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Mock StructMap
@Mapper
public interface OrderMapper {
// 即使不用static final修饰,接口里的变量默认也是静态、final的
static final OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);
@Mappings({})
OrderVO convert(OrderDTO requestDTO);
}
//===================
@Unroll
def "test convertOrders"() {
given: "Mock掉OrderMapper的静态final变量INSTANCE,并结合Spock设置动态返回值"
def orderMapper = Mock(OrderMapper.class)
Whitebox.setInternalState(OrderMapper.class, "INSTANCE", orderMapper)
orderMapper.convert(_) >> order
when:
def orders = service.convertOrders([new OrderDTO()])
then: "验证结果"
with(orders) {
it[0].orderDesc == desc
}
where: "测试数据"
order || desc
new OrderVO(type: 1) || "App端订单"
new OrderVO(type: 2) || "H5端订单"
new OrderVO(type: 3) || "PC端订单"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Dao层测试
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.5.1</version>
<scope>test</scope>
</dependency>
<!-- 测试插件 -->
<!-- 增加Groovy的maven插件、资源文件拷贝以及测试覆盖率统计插件。 -->
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.8.1</version>
<executions>
<execution>
<goals>
<goal>addSources</goal>
<goal>addTestSources</goal>
<goal>generateStubs</goal>
<goal>compile</goal>
<goal>generateTestStubs</goal>
<goal>compileTests</goal>
<goal>removeStubs</goal>
<goal>removeTestStubs</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M3</version>
<configuration>
<useFile>false</useFile>
<includes>
<include>**/*Spec.java</include>
</includes>
<parallel>methods</parallel>
<threadCount>10</threadCount>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>compile</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/resources</outputDirectory>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>post-unit-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>target/jacoco.exec</dataFile>
<outputDirectory>target/jacoco-ut</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
class Demo1Spec extends MyBaseSpec {
/**
* 直接获取待测试的mapper
*/
def personInfoMapper = MapperUtil.getMapper(PersonInfoMapper.class)
/**
* 测试数据准备,通常为sql表结构创建用的ddl,支持多个文件以逗号分隔。
*/
def setup() {
executeSqlScriptFile("com/xxx/xxx/xxx/......../schema.sql")
}
/**
* 数据表清除,通常待drop的数据表
*/
def cleanup() {
dropTables("person_info")
}
/**
* 直接构造数据库中的数据表,此方法适用于数据量较小的mapper sql测试
*/
@MyDbUnit(
content = {
person_info(id: 1, name: "abc", age: 21)
person_info(id: 2, name: "bcd", age: 22)
person_info(id: 3, name: "cde", age: 23)
}
)
def "demo1_01"() {
when:
int beforeCount = personInfoMapper.count()
// groovy sql用于快速执行sql,不仅能验证数据结果,也可向数据中添加数据。
def result = new Sql(dataSource).firstRow("select * from `person_info`")
int deleteCount = personInfoMapper.deleteById(1L)
int afterCount = personInfoMapper.count()
then:
beforeCount == 3
result.name == "abc"
deleteCount == 1
afterCount == 2
}
/**
* 直接构造数据库中的数据表,此方法适用于数据量较小的mapper sql测试
*/
@MyDbUnit(content = {
person_info(id: 1, name: 'a', age: 21)
})
def "demo1_02"() {
when:
int beforeCount = personInfoMapper.count()
def result = new Sql(dataSource).firstRow("select * from `person_info`")
int deleteCount = personInfoMapper.deleteById(1L)
int afterCount = personInfoMapper.count()
then:
beforeCount == 1
result.name == "a"
deleteCount == 1
afterCount == 0
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
编辑 (opens new window)
上次更新: 2021/12/17, 16:15:07