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

image-20211126114638661

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