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

image-20211126114638661

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

参考链接

https://javakk.com/category/spock

posted @ 2021-11-26 13:54  我的南珠里  阅读(2045)  评论(0编辑  收藏  举报