Spock虽然好用,但要应用到实际项目中还是需要注意几个问题,下面讲下我们公司在使用过程中遇到的一些问题和解决方案
版本依赖
要使用Spock首先需要引入相关依赖,目前使用下来和我们项目兼容的Spock版本是1.3-groovy-2.5
,以maven为例(gradle可以参考官网),完整的pom依赖如下:
<spock.version>1.3-groovy-2.5</spock.version>
<groovy.version>2.5.4</groovy.version>
<!-- spock -->
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>${spock.version}</version>
<scope>test</scope>
</dependency>
<!-- spock和spring集成 -->
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>${spock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<!-- spock依赖的groovy -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<type>pom</type>
<version>${groovy.version}</version>
<exclusions>
<exclusion>
<artifactId>groovy-test-junit5</artifactId>
<groupId>org.codehaus.groovy</groupId>
</exclusion>
<exclusion>
<artifactId>groovy-testng</artifactId>
<groupId>org.codehaus.groovy</groupId>
</exclusion>
</exclusions>
</dependency>
<!--groovy 编译-->
<plugin>
<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>
Spock是使用groovy语言写单测的,所以需要引入groovy-all
的依赖
在引入 groovy-all
包时排除了 groovy-test-junit5
和 groovy-testng
,这两个包和和 power mock 有冲突,在执行 mvn test 会导致NPE的问题
如果你的项目中没有用过groovy,还需要添加groovy的maven编译插件,这样才能编译我们用Spock写的单元测试
引入groovy依赖后可能会出现版本冲突的问题,因为如果你的项目引用了springboot-start-base这样的集合式jar包,它里面也会引用groovy,有可能跟我们引入的groovy包版本出现冲突,或者公司的一些框架也会引用groovy的包,如果版本不一致也有可能冲突,需要排下包
然后执行 mvn clean compile
验证下是否有冲突,如果能成功编译就没有这个问题
目前Spock的最新版本是2.0以上,在Spock 2.x 的版本里官方团队已经移除Sputnik,不再支持代理运行power mock的方式
因为Spock 2.0是基于JUnit5,我们项目以前的单元测试代码都是基于Junit4编写的,换成Junit5后,需要修改现有的java单测,比如指定代理运行,使用power mock的地方要换成Junit5的扩展语法
对现有使用Junit4 + power mock/jmockit的方式改变较大,为降低迁移成本没有使用最新的Spock2.X版本
如果你的项目之前就是使用Junit5写单测的,那么可以使用Spock2.X的版本,2.0以上版本使用power mock可以参考官方提供的解决方案:
(https://github.com/spockframework/spock/commit/fa8bd57cbb2decd70647a5b5bc095ba3fdc88ee9)
后续我也会优先在我的博客(www.javakk.com)推出 Spock2.x 版本的使用教程
创建单元测试文件
编译(mvn clean compile
)通过之后,用spock编写的groovy类型的单测代码不能放在原来的test/java目录下面
因为按照groovy的约定,默认编译groovy包下的单测,所以需要建个groovy文件夹存放spock的单测代码,如下图所示:
这样也方便区分原来Java单测和用Spock写的单测代码
另外记得别忘了标记groovy目录为测试源目录(Test Source Root),如下图:
(groovy文件夹右键 → Mark Directory as → Test Sources Root)
第一次运行spock单测代码时如果提示"no test suite exist
"的错误,可以右键recompile下
还有记得创建的单测文件类型是Groovy Class,不是Java Class类型
最后使用intellij idea的快捷键创建单元测试,在需要测试的类或方法上右键IDE的菜单,选择"Go To → Create New Test" 选择我们已经创建好的groovy文件夹:
这样就自动生成了groovy类型的单测文件了
运行单元测试
执行 mvn test
,按照上面两步的配置保证spock单测代码运行成功后可以执行 mvn clean test
命令,跑一下这个项目的单测用例
(这一步不是必须的,但如果公司加了单测覆盖率的统计时,在cicd系统发布时或merge request to release代码合并到release分支时,会先执行mvn test
类似的指令,确保所有的单元测试运行成功)
如果你的项目和我们一样既有Java单测又有Spock单测,需要确保两种单测都能执行成功(目前我们项目的spock单测和java单测在公司的CICD系统以及git上都能兼容和通过测试覆盖率要求)
另外按照Spock的规范,单测代码文件的命名应该是以Spec
为后缀的,如果你严格按照这个规范命名单测文件,比如"OrderServiceSpec.groovy
",那么需要在maven-surefire-plugin测试插件里添加以Spec为后缀的配置:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire.version}</version>
<configuration>
<includes>
<include>**/*Spec.java</include>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
但是我是直接使用IDE生成单元测试,intellij idea自动生成的单测后缀还是“Test”,所以不存在这个问题,如果你也是这样,可以忽略这个问题
单元测试规范
Spock虽然使用方便,但还是要遵循单元测试的规范来,比如单元测试一般是针对方法或类的维度去测试的,也就是说我们关注的重点是当前类或方法内部的逻辑
如果当前被测方法依赖了其他层或module的逻辑,最好mock掉,尽量不要跨层测试,这属于功能测试或集成测试的范畴
比如使用@SpringBootTest
注解,默认会把当前方法依赖的下一层引用也注入进来,其实完全可以交给Spock去控制,可以不需要SpringBootTest
Spock和Mockito注解混用问题
因为Spock并不支持Mockito和power mock的@InjectMocks
和@Mock
的组合,运行时会报错,如果你一定要使用对应的功能可以引入Mockitio为Spock专门开发的第三方工具:spock-subjects-collaborators-extension
使用@Subject
和@Collaborator
代替@InjectMocks
和@Mock
代码如下:
import spock.lang.Specification
import com.blogspot.toomuchcoding.spock.subjcollabs.Collaborator
import com.blogspot.toomuchcoding.spock.subjcollabs.Subject
class ConstructorInjectionSpec extends Specification {
public static final String TEST_METHOD_1 = "Test method 1"
SomeOtherClass someOtherClassNotToBeInjected = Mock()
@Collaborator // 类似于Mockito的@Mock
SomeOtherClass someOtherClass = Mock()
@Subject // 类似于Mockito的@InjectMocks
SomeClass systemUnderTest
def "should inject collaborator into subject"() {
given:
someOtherClass.someMethod() >> TEST_METHOD_1
when:
String firstResult = systemUnderTest.someOtherClass.someMethod()
then:
firstResult == TEST_METHOD_1
systemUnderTest.someOtherClass == someOtherClass
}
class SomeClass {
SomeOtherClass someOtherClass
SomeClass(SomeOtherClass someOtherClass) {
this.someOtherClass = someOtherClass
}
}
class SomeOtherClass {
String someMethod() {
"Some other class"
}
}
}
具体参考:
https://github.com/marcingrzejszczak/spock-subjects-collaborators-extension
我个人的建议是用PowerMockito.mock()
的方式代替注解,虽没有注解的语法简洁,但不用再引入额外的依赖
Power Mock参数匹配方法 Any()
如果在Spock里使用了power mock的mock
方法, 方法参数需要匹配的, 注意不要引用了spock的any()方法, 而应该使用power mock的any方法, 二者不能混用, 否则会报错
正确引用路径org.mockito.ArgumentMatchers
:
错误引用路径org.codehaus.groovy.runtime.DefaultGroovyMethods
:
记得前提是在powermock的api里使用参数匹配,如果是spock的mock
方法,直接使用_
下划线即可。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。