Spock实践

Spock是什么?

Spock是一款国外优秀的测试框架,基于BDD(行为驱动开发)思想实现,功能非常强大。Spock结合Groovy动态语言的特点,提供了各种标签,并采用简单、通用、结构化的描述语言,让编写测试代码更加简洁、高效。官方的介绍如下:

img

What is it? Spock is a testing and specification framework for Java and Groovy applications. What makes it stand out from the crowd is its beautiful and highly expressive specification language. Thanks to its JUnit runner, Spock is compatible with most IDEs, build tools, and continuous integration servers. Spock is inspired from JUnit, RSpec, jMock, Mockito, Groovy, Scala, Vulcans, and other fascinating life forms.

Spock是一个Java和Groovy`应用的测试和规范框架。之所以能够在众多测试框架中脱颖而出,是因为它优美而富有表现力的规范语言。Spock的灵感来自JUnit、RSpec、jMock、Mockito、Groovy、Scala、Vulcans。

简单来讲,Spock主要特点如下:

  • 让测试代码更规范,内置多种标签来规范单元测试代码的语义,测试代码结构清晰,更具可读性,降低后期维护难度。
  • 提供多种标签,比如:givenwhenthenexpectwherewiththrown……帮助我们应对复杂的测试场景。
  • 使用Groovy这种动态语言来编写测试代码,可以让我们编写的测试代码更简洁,适合敏捷开发,提高编写单元测试代码的效率。
  • 遵从BDD(行为驱动开发)模式,有助于提升代码的质量。
  • IDE兼容性好,自带Mock功能。

笔者以前实践过PowerMock、Mockito,比原来的Junit好多了,但是现在都是使用的SpringBoot开发项目,大部分的类都是Spring的Bean,使用PowerMock对类Mock并不是很好用,测试起来也算是很麻烦了,那么Spock真的好用吗?

笔者从美团的技术博客上了解到Spock的实际使用,因此在团队中,我也实践了一把。基本的使用方法,美团的技术文章已经讲的很明白了,那我就讲点不一样的。

基于Gradle的项目配置

基于kotlin的Gradle配置build.gradle.kts

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
plugins {
//引入gradle的Groovy插件
groovy
//使用jacoco生成测试报告
jacoco
}
val spockVersion = "2.0-groovy-2.5"
project {
apply(plugin = "groovy")
apply(plugin = "jacoco")

dependencies {

testImplementation("org.codehaus.groovy:groovy:2.5.9")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("junit:junit")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}

testImplementation("org.spockframework:spock-core:$spockVersion")
}
//配置测试源代码目录加上groovy的
sourceSets {

test {
java {srcDirs("src/test/java", "src/test/groovy")}
resources { srcDir("src/test/resources") }
}
}
//配置jacoco
tasks.test {
finalizedBy(tasks.jacocoTestReport)
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
}
tasks.withType<Test> {
useJUnitPlatform()
}
}

Mock模拟

我们的项目现在使用的是Kotlin,Kotlin的类默认是final的,Mock时需要修改类为open,比如

1
2
3
open class A {

}

但是如果标记了Spring的Bean注解(@Service@Repository@Component)等,其实是默认已经修改过了,不需要额外去标记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class FinLogisticsDeductionSpec extends Specification {
def finLogisticsAccountService = Mock(FinLogisticsAccountService.class)
def finLogisticsDeduction = new FinLogisticsDeductionAdvanceImpl(finLogisticsAccountService: finLogisticsAccountService)


def "测试 消费"() {
def req = new FinLogisticsDeductionExecBO(amount: new BigDecimal(100))
given: "设置消费现金100"
def cash = new BigDecimal(100)

and: "扣费服务返回扣费现金100"
finLogisticsAccountService.deduction(req, LogisticsAccountType.ADVANCE) >> cash

when: "扣费"
def result = finLogisticsDeduction.exec(req)

then: "期望结果"
with(result) {
it == cash
}
}
}

多分支条件测试

项目中经常会遇到同一个方法里面有多个if-else分支的情况,通过Spock的where标签可以非常容易的实现这个。方法名中的#amount,是直接在字符串模板中显示变量的值,可以更直观的显示在测试结果中。

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
@Unroll
def "扣费:#amount 当用户现金是:#account.availableCashAmount,红包是:#account.availableAwardAmount, 扣费后现金余额:#cash, 红包余额:#award, 返回剩余金额:#expectResult "() {
given:
def bo = new FinLogisticsDeductionExecBO(amount: amount, fromCompanyId: 1, comment: "订单扣费", deductedAfterRecharge: deductedAfterRecharge, billId: 1, billNo: "1234546")
and:
finLogisticsAccountItemRepository.selectUnusedList(account.id, false) >> accountItems
securityUtils.currentUser() >> currentUser
finLogisticsAccountItemRepository.updateById(_ as FinLogisticsAccountItemEntity) >> true

when: "扣现金"
def result = tester.loopDeduction(account, bo)

then:
with(result) {
expectResult == it
accountItems[0].availableCashBalance == cash
accountItems[0].availableAwardBalance == award

}

where: "验证分支"
amount | currentUser | account | accountItems | deductedAfterRecharge || cash | award | expectResult
10000 | currentUser() | getAccount(10000, 0) | getAccountItems(10000, 0) | true | 0 | 0 | 0
10000 | currentUser() | getAccount(0, 10000) | getAccountItems(0, 10000) | true | 0 | 0 | 0
10000 | currentUser() | getAccount(5000, 5000) | getAccountItems(5000, 5000) | true | 0 | 0 | 0
10000 | currentUser() | getAccount(4000, 5000) | getAccountItems(4000, 5000) | true | 0 | 0 | 1000
10000 | currentUser() | getAccount(6000, 5000) | getAccountItems(6000, 5000) | true | 0 | 1000 | 0
10000 | currentUser() | getAccount(10000, 0) | getAccountItems(10000, 0) | false | 0 | 0 | 0
10000 | currentUser() | getAccount(0, 10000) | getAccountItems(0, 10000) | false | 0 | 0 | 0
10000 | currentUser() | getAccount(5000, 5000) | getAccountItems(5000, 5000) | false | 0 | 0 | 0
10000 | new AuditData() | getAccount(4000, 5000) | getAccountItems(4000, 5000) | false | 0 | 0 | 1000
10000 | new AuditData() | getAccount(6000, 5000) | getAccountItems(6000, 5000) | false | 0 | 1000 | 0
0 | null | getAccount(6000, 5000) | getAccountItems(6000, 5000) | true | 6000 | 5000 | 0
0 | null | getAccount(6000, 5000) | getAccountItems(6000, 5000) | false | 6000 | 5000 | 0
100 | null | getAccount(0, 0) | getAccountItems(0, 0) | false | 0 | 0 | 100
}

image-20220211215607514

异常测试

对于方法逻辑中会抛出异常的逻辑,可以通过thrown(异常变量)方法来获取异常错误,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Unroll
def "测试退款现金不足 #expectedMessage"() {
given:
def bo = new FinLogisticsRefundExecBO(cashAmount: cashAmount, awardAmount: awardAmount, fromCompanyId: 1, comment: "订单扣费", billId: 1, billNo: "1234546")
def account = new FinLogisticsAccountEntity(id: 1)
and:
finLogisticsAccountItemRepository.selectUnusedList(account.id, false) >> accountItems
securityUtils.currentUser() >> currentUser

when:
tester.loopRefund(account, bo)
then:
def exception = thrown(expectedException)
exception.message == expectedMessage

where: "验证分支"
cashAmount | awardAmount | currentUser | accountItems || expectedException | expectedMessage
20000 | 0 | currentUser() | getAccountItems(10000, 0) || BaseException | "退款出错,现金金额不能大于账户现金余额"
20000 | 0 | currentUser() | getAccountItems(0, 0) || BaseException | "退款出错,现金金额不能大于账户现金余额"
0 | 20000 | currentUser() | getAccountItems(0, 10000) || BaseException | "退款出错,红包金额不能大于账户红包余额"
10000 | 11000 | currentUser() | getAccountItems(10000, 10000) || BaseException | "退款出错,红包金额不能大于账户红包余额"
0 | 100 | currentUser() | getAccountItems(0, 0) || BaseException | "退款出错,红包金额不能大于账户红包余额"

}

推荐文章