Stay hungry, Stay foolish

0%

Spock单元测试入门

简介

Spock 是一款用 Groovy 写的用于 Java 和 Groovy 应用测试的框架
相对于 JUnit 和 Mockito 的复杂和繁冗来说,无疑会让人眼前一亮。笔者正是在几年前被其吸引并一直在内部进行推广

优势

  • 语法优雅
    • 简洁
    • 表达能力强
    • 可读性强
  • 效率高(单元测试还是很适合用动态语言来写的)

依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>2.0-M1-groovy-2.5</version>
<scope>test</scope>
</dependency>
<dependency> <!-- use a specific Groovy version rather than the one specified by spock-core -->
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>2.5.8</version>
</dependency>

配置

gmavenplus plugin

编译 Groovy 脚本用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<plugin>
<!-- The gmavenplus plugin is used to compile Groovy code -->
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compileTests</goal>
</goals>
</execution>
</executions>
</plugin>

maven-surefire-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M1</version>
<configuration>
<!--<skipTests>true</skipTests>-->
<includes>
<include>**/*Test.java</include>
<include>**/*Spec.java</include>
</includes>
<excludes>
<exclude>**/module/room/viewer/service/impl/RoomViewerServiceImplTest.java</exclude>
</excludes>
</configuration>
</plugin>
  • 虽然 Spock 的单元测试是 Groovy 的后缀,但是这儿仍然要用 Java 的后缀

  • 由于 Groovy 和 Java 代码编译后都是class的后缀,所以两者的命名不能相同

  • 最好用不同的测试目录

使用

依赖注入

可以在setup/setupSpec里对要测试对象进行依赖注入

1
2
3
4
5
6
7
8
9
10
11
AccountService accountService = Mock()
RedisHelper redisHelper = Mock()
AddLogPOMapper addLogPOMapper = Mock()

private AccountManager accountManager = new AccountManager();

void setup() {
accountManager.accountService = accountService
accountManager.redisHelper = redisHelper
accountManager.addLogPOMapper = addLogPOMapper
}

准备测试

1
2
3
4
5
6
def fakeNum = 100
def realNum = 200

given: "mock账户余额"
1 * accountService.queryAccount(_) >> new QueryResultDTO(balance: fakeNum)
1 * accountService.queryAccount(_) >> new QueryResultDTO(balance: realNum)

given & andspock通过这两个关键字来修饰测试前的准备,比如mock的数据、方法等

1* 放在方法前用于表达方法的执行次数

>> 后面跟方法的返回内容

执行受试方法

1
2
when:
AccountDTO accountDTO = accountManager.getAccount(123456)

验证结果

1
2
3
4
5
6
then:
with(accountDTO){
realNum == realNum
fakeNum == fakeNum
num == realNum + fakeNum
}

实例

修改并返回传参

1
2
given:
1 * addLogPOMapper.insertSelective(_ as AddLogPO) >> {AddLogPO po -> po.id = 123}

验证方法的执行

验证参数值

1
2
then:
1 * msgStoreManger.insertSelective({ MessagePO messagePO -> messagePO.getOptPlatform() == PlatformEnum.SINA.name()})

验证参数类型

1
2
then:
1 * redisHelper.expire(_ as String, _ as Long, _ as TimeUnit)

验证异常

1
2
3
then:
def ex = thrown(Exception)
ex.message == LotteryGameExceptionEnum.EXISTS_UNFINISHED_LOTTERY.getMessage()

如果想使用where同时验证正常和异常两种情况的话,就需要一点点 Hack 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class GameUnfinishedValidatorSpec extends Specification {
class Success extends Exception{}
def "Valid"() {
given:
gamePOMapper.selectUnfinishedItem(_ as Long) >> item

when:
unfinishedValidator.valid(new GameParam(roomId:1L))
throw new Success()

then:
def ex = thrown(expectedException)
ex.message == expectedMessage

where:
item | expectedException | expectedMessage
new LotteryGamePO(roomId: 1L) | Exception | LotteryGameExceptionEnum.EXISTS_UNFINISHED_LOTTERY.getMessage()
null | Success | null
}
}

1
def ruleParam = new GameRuleParam(isFollow: false, isShare: true, isComment: false)

这种写法虽然效率高,但是在原始类上对变量名进行重构后,这儿并不会更新

据说打赏我的人,代码没有BUG