Spock+powermock单元测试笔记
Spock+powermock的单元测试笔记(持续更新中ing)
本篇结合我自身的工作经验做一个简单的单测总结
为什么
为什么要做单元测试
单测其实分为两种,一种是写业务代码前写单测,一种是写业务代码后写单测。
一般来说,应该在写业务代码前写单测,开发前写单测可以帮助开发者从业务着手缕清编码思路,不至于跑偏,后人也可以借单测来了解一部分业务逻辑。
而写业务代码后写单测也必不可少,因为要提高单测的行覆盖率和分支覆盖率,覆盖到每一行和每一个分支,以便之后再修改这块代码逻辑时可以自行从容。
为什么要使用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,不过上面这些都是工具,选一个自己熟悉趁手的就行
开始
引入依赖
版本号:
1.3-groovy-2.5
2.5.4
2.0.0
主要依赖:
junit
junit
4.12
test
org.springframework
spring-test
4.3.2.RELEASE
test
org.spockframework
spock-core
${spock.version}
test
org.spockframework
spock-spring
${spock.version}
test
net.bytebuddy
byte-buddy
1.9.3
test
org.codehaus.groovy
groovy-all
pom
${groovy.version}
test
groovy-test-junit5
org.codehaus.groovy
groovy-testng
org.codehaus.groovy
org.mockito
mockito-core
2.23.0
test
net.bytebuddy
byte-buddy
byte-buddy-agent
net.bytebuddy
org.powermock
powermock-api-mockito2
${powermock.version }
test
org.powermock
powermock-core
${powermock.version}
test
org.powermock
powermock-module-junit4
${powermock.version}
test
插件(包含JaCoCo):
org.codehaus.gmavenplus
gmavenplus-plugin
1.4
compile
testCompile
org.apache.maven.plugins
maven-surefire-plugin
2.22.2
true
**/*Spec.java
**/*Test.java
org.jacoco
jacoco-maven-plugin
0.8.2
prepare-agent
report
test
report
要注意版本冲突,要自己去排包,上面这部分依赖不一定适合所有人
基础
所有的测试类需要继承Specification
class MyFirstTest extends Specification {
// fields
// fixture methods
// feature methods
// helper methods
}
固定方法
?def setupSpec() {} // 只运行一次 在第一个Feature 执行前
?def setup() {} // 每个Feature 运行前
?def cleanup() {} //每个Feature 运行后
?def cleanupSpec() {} //只运行一次 在最后一个Feature 执行后
调用顺序
?super.setupSpec
?sub.setupSpec
?super.setup
?sub.setup
?待执行的Feature方法
?sub.cleanup
?super.cleanup
?sub.cleanupSpec
?super.cleanupSpec
Feature方法
包含以下四个部分
?Setup
?Stimulus
?Response
?Cleanup
Blocks
?given:输入条件(前置参数)
?when、then |expect
when: 执行行为(mock接口、真实调用)
then:输出条件(验证结果)
?where、with:既能覆盖多种分支,又可以对复杂对象的属性进行验证
where:通过表格的方式测试多种分支
with:验证复杂返回对象使用
?and:衔接上个标签,补充的作用
?cleanup : 清理必要的资源,一定会执行的block
例子
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)]
}
高级
mock
spock自带mock,使用起来也非常简单
// 前置mock,被mock后的对象不会走原来的方法,会直接跳过,若有返回值会直接返回null
def userDao = Mock(UserDao)
// 在单测中直接使用 >> obj,则表示直接放回obj
userDao.getUserInfo() >> users
spy
// 前置spy,被spy后的对象会走原来的方法,但如果有对其中的方法做mock,则会直接放回mock后的结果obj
def userDao = Spy(UserDao)
// 在单测中直接使用 >> obj,则表示直接放回obj,其他方法会正常走
userDao.getUserInfo() >> users
stub
存根是使协作者以某种方式响应方法调用的行为。当存根方法时,你不关心该方法是否以及将被调用多少次;你只是希望它在被调用时返回一些值
Stub()
存根方法也是一个虚拟类,比Mock()
方法更简单一些,只返回事先准备好的假数据,而不提供交互验证(即该方法是否被调用以及将被调用多少次)。使用存根Stub只能验证状态(例如测试方法返回的结果数据是否正确,list大小等,是否符合断言)。
所以Mock比Stub的功能更多一些,但如果我们只是验证结果使用Stub就足够了,用法和Mock一样,而且更轻量一些。
一般情况下,我们都只需要使用到mock就行,如果遇到要mock被测类的其他方法时,可以考虑使用spy
exception测试
Spock内置thrown()方法,可以捕获调用业务代码抛出的预期异常并验证
then: "捕获异常并设置需要验证的异常值"
def exception = thrown(ClientTimeOutException)
exception.errorCode == expectedErrCode
exception.errorMessage == expectedMessage
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]
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静态类的准备,第二个是抑制一些类的初始化
抽象方法
抽象方法/父类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
注解整理
@UnRoll
@Unroll注解表示展开where标签下面的每一行测试,作为单独的case跑
@Shared
在测试方法之间共享的数据
@RunWith
一个运行器
@RunWith(JUnit4.class) // 就是指用JUnit4来运行
@RunWith(SpringJUnit4ClassRunner.class) // 让测试运行于Spring测试环境
@RunWith(Suite.class) // 的话就是一套测试集合
@ContextConfiguration
Spring整合JUnit4测试时,使用注解引入多个配置文件
单个文件
@ContextConfiguration(Locations="classpath:applicationContext.xml")
@ContextConfiguration(classes = SimpleConfiguration.class)
多个文件时,可用{}
@ContextConfiguration(locations = { "classpath:spring1.xml", "classpath:spring2.xml" })
@MockBean
对于一些应用的外部依赖需要进行一些Mock
处理 -> 会自动注入
危害:https://segmentfault.com/a/1190000014122154
@Mock和@MockBean和Mockito.mock()的区别:https://blog.csdn.net/weixin_34101229/article/details/91395871
@SpyBean
类上
未完待续。。。
注意事项
① spock单测目录要放在groovy目录下
② spock里面的mock匹配任意参数,使用下划线 _ 即可
③ 注意jar包冲突,有些单测无法运行mock失败,不一定是编码问题,可能就是依赖冲突了,或者依赖的版本不对
④ Spock并不支持Mockito和power mock的@InjectMocks
和@Mock
的组合,运行时会报错,如果你一定要使用对应的功能可以引入Mockitio为Spock专门开发的第三方工具:spock-subjects-collaborators-extension
使用@Subject
和@Collaborator
代替@InjectMocks
和@Mock
参考链接
https://javakk.com/category/spock