Spock+powermock单元测试笔记(持续更新中)
本篇结合我自身的工作经验做一个简单的单测总结
为什么
为什么要做单元测试
单测其实分为两种,一种是写业务代码前写单测,一种是写业务代码后写单测。
一般来说,应该在写业务代码前写单测,开发前写单测可以帮助开发者从业务着手缕清编码思路,不至于跑偏,后人也可以借单测来了解一部分业务逻辑。
而写业务代码后写单测也必不可少,因为要提高单测的行覆盖率和分支覆盖率,覆盖到每一行和每一个分支,以便之后再修改这块代码逻辑时可以自行从容。
为什么要使用spock
spock相较于我们之前写的junit+mockito+powermock/jmockito来说主要有以下几点优势
① 代码量少:spock要结合groovy来使用,groovy可以直接编译成.class类在JVM中运行。使用groovy开发,代码更简洁易懂。当然学习groovy本身就含有额外的学习成本,好在groovy语法与 Java 语言的语法很相似,学习起来非常轻松,学习成本不大。
② 语义性好,可读性高:基于BDD思想,使用语义标签严格控制流程,是一种强制性的约束
③ 自带mock:相较于junit,spock自带mock,当然目前spock做一些高级mock比如静态类,但也可以引入powermock或jmockito来使用
为什么要使用powermock
主要是为了弥补spock无法做高级mock比如静态类的缺陷
相较于jmockito,powermock更重量级,更慢,但是powermock功能更强大,编码更简单,IDEA支持更好,下面是几种常见单元测试工具的对比(jmockito编码可读性不好,与平时编码习惯不同)
Mock工具 | Mock原理 | 最小Mock单元 | 被Mock方法限制 | 使用难度 | IDE支持 |
---|---|---|---|---|---|
Mockito | 动态代理 | 类 | 不能Mock私有/静态和构造方法 | 较容易 | 很好 |
Spock | 动态代理 | 类 | 不能Mock私有/静态和构造方法 | 较复杂 | 一般 |
PowerMock | 自定义类加载器 | 类 | 任何方法皆可 | 较复杂 | 较好 |
JMockit | 运行时字节码修改 | 类 | 不能Mock构造方法 | 较复杂 | 一般 |
TestableMock | 运行时字节码修改 | 方法 | 任何方法皆可 | 很容易 | 一般 |
表格来自TestableMock官网,以后有机会可以尝试下TestableMock,不过这些都是工具,选一个自己熟悉趁手的就行
开始
引入依赖
版本号:
<spock.version>1.3-groovy-2.5</spock.version>
<groovy.version>2.5.4</groovy.version>
<powermock.version>2.0.0</powermock.version>
主要依赖:
<!--test start-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.2.RELEASE</version>
<scope>test</scope>
</dependency>
<!-- 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>
<!-- spock的mock需要,版本与spock-core版本需同步更新,可参考mvnrepository.com -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.9.3</version>
<scope>test</scope>
</dependency>
<!-- spock依赖的groovy -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<type>pom</type>
<version>${groovy.version}</version>
<scope>test</scope>
<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>
<!-- PowerMock -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
</exclusion>
<exclusion>
<artifactId>byte-buddy-agent</artifactId>
<groupId>net.bytebuddy</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-core</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
插件(包含JaCoCo):
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<skip>true</skip>
<includes>
<include>**/*Spec.java</include>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
<!--JaCoCo-->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
要注意版本冲突,要自己去排包,上面这部分依赖不一定适合所有人
基础
1、所有的测试类都需要继承Specification
class MyFirstTest extends Specification {
// fields
// fixture methods
// feature methods
// helper methods
}
2、固定的几个方法
•def setupSpec() {} // 只运行一次 在第一个Feature 执行前
•def setup() {} // 每个Feature 运行前
•def cleanup() {} //每个Feature 运行后
•def cleanupSpec() {} //只运行一次 在最后一个Feature 执行后
3、几个固定方法的调用顺序
•super.setupSpec
•sub.setupSpec
•super.setup
•sub.setup
•待执行的Feature方法
•sub.cleanup
•super.cleanup
•sub.cleanupSpec
•super.cleanupSpec
4、Feature方法包含的四个部分
•Setup
•Stimulus
•Response
•Cleanup
5、核心Blocks
•given:输入条件(前置参数)
•when、then |expect
when: 执行行为(mock接口、真实调用)
then:输出条件(验证结果)
•where、with:既能覆盖多种分支,又可以对复杂对象的属性进行验证
where:通过表格的方式测试多种分支
with:验证复杂返回对象使用
•and:衔接上个标签,补充的作用
•cleanup : 清理必要的资源,一定会执行的block
6、一个示例
def userDao = Mock(UserDao)
def "当输入的用户id为:#uid 时返回的邮编是:#postCodeResult,处理后的电话号码是:#telephoneResult"() {
given: "mock掉接口返回的用户信息"
userDao.getUserInfo() >> users
when: "调用获取用户信息方法"
def response = userService.getUserById(uid)
then: "验证返回结果是否符合预期值"
with(response) {
postCode == postCodeResult
telephone == telephoneResult
}
where: "表格方式验证用户信息的分支场景"
uid | users || postCodeResult | telephoneResult
1 | getUser("上海", "13866667777") || 200000 | "138****7777"
1 | getUser("北京", "13811112222") || 100000 | "138****2222"
2 | getUser("南京", "13833334444") || 0 | null
}
def getUser(String province, String telephone){
return [new UserDTO(id: 1, name: "张三", province: province, telephone: telephone)]
}
高级
1、mock
spock自带mock,使用起来也非常简单
// 前置mock,被mock后的对象不会走原来的方法,会直接跳过,若有返回值会直接返回null
def userDao = Mock(UserDao)
// 在单测中直接使用 >> obj,则表示直接放回obj
userDao.getUserInfo() >> users
2、spy
// 前置spy,被spy后的对象会走原来的方法,但如果有对其中的方法做mock,则会直接放回mock后的结果obj
def userDao = Spy(UserDao)
// 在单测中直接使用 >> obj,则表示直接放回obj,其他方法会正常走
userDao.getUserInfo() >> users
3、stub
存根是使协作者以某种方式响应方法调用的行为。当存根方法时,你不关心该方法是否以及将被调用多少次;你只是希望它在被调用时返回一些值
Stub()
存根方法也是一个虚拟类,比Mock()
更简单一些,只返回事先准备好的假数据,不提供交互验证(即该方法是否被调用以及将被调用多少次),使用存根Stub只能验证状态(例如测试方法返回的结果数据是否正确,list大小,是否符合断言等)。
所以Mock比Stub的功能更多一些,但如果我们只是验证结果使用Stub就足够了,用法和Mock一样,而且更轻量一些。
一般情况下,我们都只需要使用到mock就行,如果遇到要mock被测类的其他方法时,可以考虑使用spy
4、exception测试
Spock内置thrown()方法,可以捕获调用业务代码抛出的预期异常并验证
then: "捕获异常并设置需要验证的异常值"
def exception = thrown(ClientTimeOutException)
exception.errorCode == expectedErrCode
exception.errorMessage == expectedMessage
5、viod
void方法的测试不能像前面几篇介绍的那样在then标签里验证返回结果,因为void方法没有返回值
一般来说无返回值的方法,内部逻辑会修改入参的属性值,比如参数是个对象,那代码里可能会修改它的属性值,虽然没有返回,但还是可以通过校验入参的属性来测试void方法
还有一种更有效的测试方式,就是验证方法内部逻辑和流程是否符合预期,比如:
- 应该走到哪个分支逻辑?
- 是否执行了这一行代码?
- for循环中的代码执行了几次?
- 变量在方法内部的变化情况?
then: "验证调用获取最新汇率接口的行为是否符合预期: 一共调用2次, 第一次输出的汇率是0.1413, 第二次是0.1421"
2 * moneyDAO.getExchangeByCountry(_) >> 0.1413 >> 0.1421
上面这个就是验证某方法在待测方法内部执行了2次,2次的结果分别是什么,验证结果还有下面这种写法
使用>>>后面接中括号,中括号里面的顺序就是执行结果的顺序
then: "验证调用获取最新汇率接口的行为是否符合预期: 一共调用2次, 第一次输出的汇率是0.1413, 第二次是0.1421"
2 * moneyDAO.getExchangeByCountry(_) >>> [0.1413, 0.1421]
6、static方法
该部分结合powemock使用即可,在此不介绍powermock的使用语法,介绍一下spock+powermock结合使用的方式
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([LogUtils.class, IDNumberUtils.class])
@SuppressStaticInitializationFor(["com.javakk.spock.util.LogUtils"])
powermock需要使用@RunWith(PowerMockRunner.class)来进行初始化,可惜的是目前单测@RunWith里面只能加一个启动类,要结合使用spock的话,可以用到powermock的@PowerMockRunnerDelegate注解,将Sputnik.class加进来
另外两个注解就是power自己要使用到的了,第一个是要mock静态类的准备,第二个是抑制一些类的初始化
7、抽象方法
抽象方法/父类super方法的mock我们可以借用powermock的能力来实现,例子如下
given:
// Child child = PowerMockito.spy(new Child())
Child child = PowerMockito.mock(Child.class)
// mock掉抽象类的parentMethod, 返回动态mock值:mockParentReturn
PowerMockito.when(child.parentMethod()).thenReturn(parentValue)
PowerMockito.when(child.doSomthing()).thenCallRealMethod()
expect:
child.doSomthing() == result
注解整理
spring提供的:@RunWith、@ContextConfiguration、@MockBean、#SpyBean...
1、@RunWith
一个运行器
@RunWith(JUnit4.class) // 就是指用JUnit4来运行
@RunWith(SpringJUnit4ClassRunner.class) // 让测试运行于Spring测试环境
@RunWith(Suite.class) // 的话就是一套测试集合
2、@ContextConfiguration
Spring整合JUnit4测试时,使用注解引入多个配置文件
单个文件
@ContextConfiguration(Locations="classpath:applicationContext.xml")
@ContextConfiguration(classes = SimpleConfiguration.class)
多个文件
@ContextConfiguration(locations = { "classpath:spring1.xml", "classpath:spring2.xml" })
3、@MockBean
对于一些应用的外部依赖需要进行一些Mock
处理 -> 会自动注入
危害:https://segmentfault.com/a/1190000014122154
@Mock和@MockBean和Mockito.mock()的区别:https://blog.csdn.net/weixin_34101229/article/details/91395871
4、@SpyBean
类上
spock提供的:@UnRoll、@Shared...
1、@UnRoll
@Unroll注解表示展开where标签下面的每一行测试,作为单独的case跑
2、@Shared
在测试方法之间共享的数据
未完待续。。。
注意事项
① spock单测目录要放在groovy目录下
② spock里面的mock匹配任意参数,使用下划线 _ 即可
③ 注意jar包冲突,有些单测无法运行mock失败,不一定是编码问题,可能就是依赖冲突了,或者依赖的版本不对
④ Spock并不支持Mockito和power mock的@InjectMocks
和@Mock
的组合,运行时会报错,如果你一定要使用对应的功能可以引入Mockitio为Spock专门开发的第三方工具:spock-subjects-collaborators-extension
使用@Subject
和@Collaborator
代替@InjectMocks
和@Mock