Spock介绍
Spock
是国外一款优秀的测试框架,基于BDD
(行为驱动开发)思想实现,功能非常强大。Spock
结合Groovy
动态语言的特点,提供了各种标签,并采用简单、通用、结构化的描述语言,让编写测试代码更加简洁、高效。Spock
作为测试框架,在开发效率、可读性和维护性方面均取得了不错的收益。
修改项目配置
项目用gradle
管理,用的是7.4.2
版本,spock
用的是2.0-M3-groovy-3.0
版本。
配置修改
修改文件build.gradle
并添加依赖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
26List spockTest = [
"org.spockframework:spock-core:2.0-M3-groovy-3.0",
"org.spockframework:spock-spring:2.0-M3-groovy-3.0",
"org.mockito:mockito-core:4.3.1",
"org.mockito:mockito-inline:4.3.1",
"org.springframework.boot:spring-boot-starter-test"
]
dependencies {
implementation 'org.codehaus.groovy:groovy:3.0.5'
testImplementation spockTest
}
sourceSets {
main {
java { srcDirs = ['src/main/java'] }
resources { srcDirs = ['src/main/resources'] }
}
test {
java { srcDirs = ['src/test/groovy'] }
resources { srcDirs = ['src/test/resources'] }
}
}
apply from: 'test.gradle'
修改test.gradle
,这步主要是和jacoco
结合生成报告(带覆盖率,用的jacoco
的offline
特性)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
103apply from: 'testFilesConf.gradle'
apply plugin: 'java'
apply plugin: "groovy"
apply plugin: "jacoco"
jacoco {
toolVersion = "0.8.7"
reportsDir = file("$buildDir/customJacocoReportDir")
}
configurations {
jacocoAnt
jacocoRuntime
}
test {
useJUnitPlatform()
maxHeapSize = "1G"
jacoco {
destinationFile = file("$buildDir/jacoco/test.exec")
classDumpDir = file("$buildDir/jacoco/classpathdumps")
}
testLogging {
afterSuite { desc, result ->
if (!desc.parent) {
println "Unit Tests: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
}
}
}
}
def excludedSourcesPattern = ['**/entity/**', '**/dto/**']
jacocoTestReport {
reports {
html.enabled true
csv.enabled false
xml.enabled true
xml.destination file("$buildDir/jacocoHtml/jacocoXml.xml")
html.destination file("$buildDir/reports/jacoco/test/html")
}
afterEvaluate {
getClassDirectories().setFrom(
classDirectories.files.collect {
fileTree(dir: it, excludes: excludedSourcesPattern)
}
)
}
}
def compileClassPath = [ "${project.buildDir}/classes/java/main"]
task instrument(dependsOn: ['classes']) {
ext.outputDir = buildDir.path + '/intermediates/classes-instrumented/Java'
doLast {
ant.taskdef(name: 'instrument',
classname: 'org.jacoco.ant.InstrumentTask',
classpath: configurations.jacocoAnt.asPath)
ant.instrument(destdir: outputDir) {
compileClassPath.each {
fileset(dir: it)
}
}
}
}
gradle.taskGraph.whenReady { graph ->
if (graph.hasTask(instrument)) {
tasks.withType(Test) {
doFirst {
systemProperty 'jacoco-agent.destfile', buildDir.path + '/jacoco/test.exec'
classpath = files(instrument.outputDir) + classpath + configurations.jacocoRuntime
}
}
}
}
task jacocoReport(dependsOn: ['instrument', 'test']) {
doLast {
ant.taskdef(name: 'report',
classname: 'org.jacoco.ant.ReportTask',
classpath: configurations.jacocoAnt.asPath)
ant.report() {
executiondata {
ant.file(file: buildDir.path + "/jacoco/test.exec")
}
structure(name: project.name) {
classfiles {
compileClassPath.each {
fileset(dir: it)
}
}
sourcefiles {
fileset(dir: 'src/main/java')
}
}
html(destdir: buildDir.path + '/reports/jacoco')
}
}
}
check.dependsOn jacocoTestReport
对Spring的封装
这边对Spring做了一个封装,可以做成一个测试的maven模块,先写一个自定义注解,主要是用作Spring容器的启动,同时制定启动的配置文件为test1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @version 1.0
* @Author: dinghuang
* @Description:
* @Date: since 2022/5/30 16:03
* @Modify By: dinghuang
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootTest(webEnvironment = RANDOM_PORT, classes = ServerApplication.class)
@ActiveProfiles("test")
@Stepwise
public @interface CustomTest {
}
这个时候我们在test的resource下面的application.properties
修改一下配置1
spring.profiles.active=test
接下来就是写代码了
代码编写
代码写在test
目录下,跟junit
不一样的是,目录结构是:test->groovy->xxxx.xxx.xxx
需要
IDEA
把groovy
的mark directory
设置成test root
单元测试
这边不懂Spock的语法的,可以百度下,这边直接略过
先看业务代码,业务很简单,查一个字典表返回数据1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* @Version 1.0
* @Author: dinghuang
* @Description:
* @Date: since 2022/4/1 18:05
* @Modify By: dinghuang
*/
public class SysDictService {
private static final Logger LOGGER = LoggerFactory.getLogger(SysDictService.class);
private SysDictDao sysDictDao;
public String getDictName(String dict) {
SysDict sysDict = sysDictDao.selectByPrimaryKey(dict);
return sysDict.getDictname();
}
}
Mock模拟
1 |
|
Where使用
正常我们单元测试率有个分支覆盖率指标,如果用junit写太麻烦了,用spock可以非常简洁,假如字典的逻辑不同的dictName有不同的处理逻辑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
/**
*
* @Author: dinghuang
* @Description:
* @Date: since 2022/5/30 16:18
* @version 1.0
* @Modify By: dinghuang
*
*/
class TestControllerTest extends Specification {
SysDictTestService sysDictTestService
SysDictDao sysDictDao = Mock()
def 'Where测试'() {
given:
sysDictDao.selectByPrimaryKey(a) >> d
when:
def b = sysDictTestService.getDictName(a)
then:
b == c
where:
a | d || c
'10000' | new SysDict('系统') || '系统'
'10004' | new SysDict('多点登录处理方式') || '多点登录处理方式'
}
}
这里可以用@Unroll
注解,可以把每一次调用作为一个单独的测试用例运行,这样运行后的单元测试结果更加直观:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17@Unroll
def 'Where测试'() {
given:
sysDictDao.selectByPrimaryKey(a) d
when:
def b = sysDictTestService.getDictName(a)
then:
b == c
where:
a | d || c
'10000' | new SysDict('系统') || '系统'
'10004' | new SysDict('多点登录处理方式') || '多点登录处理方式'
}
测试异常
先修改一下业务代码,如下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
/**
* @Version 1.0
* @Author: dinghuang
* @Description:
* @Date: since 2022/4/1 18:05
* @Modify By: dinghuang
*/
public class SysDictTestService {
private static final Logger LOGGER = LoggerFactory.getLogger(SysDictTestService.class);
private SysDictDao sysDictDao;
public String getDictName(String dict) {
if ("测试异常".equals(dict)) {
throw new BusinessException(ErrorCodeEnum.SYS_ERROR);
}
SysDict sysDict = sysDictDao.selectByPrimaryKey(dict);
return sysDict.getDictname();
}
}
写异常测试类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
/**
*
* @Author: dinghuang
* @Description:
* @Date: since 2022/5/30 16:18
* @version 1.0
* @Modify By: dinghuang
*
*/
class TestControllerTest extends Specification {
SysDictTestService sysDictTestService
def '测试异常'() {
given:
def dict = '测试异常'
when:
sysDictTestService.getDictName(dict)
then:
def exception = thrown(BusinessException)
exception.errorCode == '21379999'
exception.message == 'bizErrCode=21379999,bizErrMsg=系统异常'
}
}
静态方法Mock
先修改一下业务代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @Version 1.0
* @Author: dinghuang
* @Description:
* @Date: since 2022/4/1 18:05
* @Modify By: dinghuang
*/
public class SysDictTestService {
private static final Logger LOGGER = LoggerFactory.getLogger(SysDictTestService.class);
private SysDictDao sysDictDao;
public String getDateStr() {
return DateUtil.getCurrentSysDate();
}
}
1 | public class DateUtil { |
静态方法测试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
/**
*
* @Author: dinghuang
* @Description:
* @Date: since 2022/5/30 16:18
* @version 1.0
* @Modify By: dinghuang
*
*/
class TestControllerTest extends Specification {
SysDictTestService sysDictTestService
MockedStatic<DateUtil> dateUtilMockedStatic
void setup() {
dateUtilMockedStatic = Mockito.mockStatic(DateUtil.class)
// mock静态类
}
def '测试异常'() {
given:
Mockito.when(DateUtil.getCurrentSysDate()).thenReturn("1991-01-02")
when:
def date = sysDictTestService.getDateStr()
then:
date == "1991-01-02"
}
}
测试sql
这个原理是在每个单元测试可以写一个前置后置处理方法,比如跑测试A,会自动跑测试A的前置,把数据库初始化进去,数据初始化进去,用的是H2内存数据库,但是这个东西对特定数据库的一些语法不支持,所以不太建议这么做,简单的sql的确可以测试出问题
1 | /** |
报告
正常运行完成gradle test
会生成报告在路径build\reports\tests\test\index.html
,这个是html的,没有覆盖率的情况,如图所示:
运行我们test.gradle
中自己写的task,gradle jacocoReport
,会生成单元测试覆盖报告在路径build\reports\jacoco\index.html
,如图所示