Spock是国外的测试框架,其设计灵感来自JUnit、Mockito、Groovy,可以用于Java和Groovy应用的测试。

Spock简介

最近发现了一款写法简洁高效,一个单测方法可以测试多组测试数据,且测试结果一目了然的单测框架Spock。Spock是国外的测试框架,其设计灵感来自JUnit、Mockito、Groovy,可以用于Java和Groovy应用的测试。尽管Spock写单测,需要使用groovy语言,但是groovy语言是一种弱类型,且兼容Java语法,写法超级简单,相信你看过这篇文档,就会用groovy写单测啦。

简单介绍下Junit、Mock(Jmock、Mockito、PowerMock、Spock)单测框架的对比:

  1. JUnit适用于没有外部依赖服务、或者外部依赖服务较少的简单类的单测,对于有外部依赖服务的类、或者对运行环境有要求的类,Junit模拟外部依赖、环境非常耗时。

  2. Mock类型的单测方式会解决外部依赖不容易模拟的问题,常见的Mock有Jmock、Mockito、PowerMock、Spock等,简单对比下几种Mock:

  1. Jmock:通过模拟外部依赖对象来模拟其的行为,从而隔离不关心的外部依赖对象,使单测专注于被测方法的逻辑是否正确。

  2. Mockito:Mockito是Jmock的升级版,Jmock需要在执行前记录期望的行为,而Mockito只需要在执行后校验哪些函数被调用即可,写法更干净、简洁。

  3. PowerMock:PowerMock是在Mockito的基础上,又支持了对静态方法、私有方法、构造函数的模拟。但是由于PowerMock会篡改字节码,导致测试时的字节码与编译出来的字节码不同,而单测的覆盖率大多是基于字节码统计的,导致PowerMock编写的单测不能被统计进覆盖率,这是PowerMock的硬伤。

  4. Spock:Spock设计灵感来自JUnit、Mockito、Groovy,可以用于Java和Groovy应用的测试,其写法简洁高效,一个单测方法可以测试多组测试数据,且测试结果一目了然,而Mockito不支持一个测试用例执行多组测试数据。尽管Spock写单测,需要使用groovy语言,但是groovy语言是一种弱类型,且兼容Java语法,写法超级简洁,容易上手。

在日常需求开发中,需求都是依赖很多外部的服务,如数据库、中间件等,所以大多会选择Mock的方式编写单测。PowerMock的缺点是单测覆盖率统计的可能不准确,所以Mockito和Spock是被大家常用的。而Spock的写法要比Mockito简单很多。下面介绍下Spock写法,另外在展示Spock的一个测试用例可以执行多组测试数据时,会给出Mockito对应的写法,经过对比后,你会发现Spock是真的香!

Spock环境配置

引入jar包

<!--groovy单测框架-->        <dependency><groupId>org.codehaus.groovy</groupId><artifactId>groovy-all-tests</artifactId><version>2.0.0-rc-3</version></dependency><!-- Mandatory dependencies for using Spock test framework --><dependency><groupId>org.spockframework</groupId><artifactId>spock-core</artifactId><version>1.3-groovy-2.4</version></dependency><dependency><groupId>org.spockframework</groupId><artifactId>spock-spring</artifactId><version>1.3-groovy-2.4</version><scope>test</scope></dependency>

配置插件

<plugin><!--groovy plugin--><groupId>org.codehaus.gmavenplus</groupId><artifactId>gmavenplus-plugin</artifactId><version>1.4</version><extensions>true</extensions><executions><execution><goals><goal>compile</goal><goal>testCompile</goal></goals></execution></executions><configuration><!-- spock单测文件路径 --><testSources><testSource><directory>${project.basedir}/src/test/java</directory><includes><include>**/*.groovy</include></includes></testSource><testSource><directory>${project.basedir}/src/test/groovy</directory><includes><include>**/*.groovy</include></includes></testSource></testSources></configuration></plugin>

Spock用法

  given-expect-where

given-expect-where组合常用于被测方法包含多个逻辑分支的测试,其中,

  1. given块:用于写测试前的准备工作,例如我们只需要测试方法A的逻辑是否有问题,而方法A依赖外部类的方法B,那么可以在given块,模拟方法B的返回值

  2. expect块:用于写测试期望的结果,只能写判断式,如a==b,而参数a、b是在where块进行赋值的

  3. where块:用于写expect块断言的参数(测试数据、及期望返回值),where块可以写多组测试数据、和期望返回值,对于被测方法的逻辑有多个分支的情况,Spock的where特点,可以只写一次单测代码,就能模拟多组测试是否正确,大大节省写单测时间

如下是被测方法TaskService.getTask(),不同的系统环境,获取到的任务也不同,为了确保各个环境查询任务的正确性,需要覆盖所有分支。

public class TaskService {/*** 环境*/@Value("${spring.current.env}")private String env;/*** 任务 服务类*/@Resourceprivate ITaskRepository taskRepository;/*** 查询任务信息(根据环境,查询任务)** @return 任务*/public Result<Task> getTask() {if (EnvEnum.isDaily(env)) {// 日常环境,任务取值 本方法直接newTask task = new Task();task.setInput(EnvEnum.DAILY.name() + " 任务");return Result.isOk(task);}if (EnvEnum.isPre(env)) {// 预发环境,任务取值于 本类的方法的返回值return getPreTask();}try {// 线上环境,任务取值于 另一个类的方法的返回值return Result.isOk(taskRepository.getTask(1L));} catch (Exception ex) {// 异常return Result.onError("异常任务");}}/*** 查询预发环境的任务** @return 任务*/public Result<Task> getPreTask() {Task task = new Task();task.setInput("TaskService getInternalTask 任务" + EnvEnum.PRE.name());return Result.isOk(task);}
}
  • Mockito单测用例

如下是通过Mockito写的getTask()方法的测试用例,可以看到一共有3个测试用例,分别是日常、预发、正式环境,我们以正式环境的测试用例为例说下Mockito的用法

  1. @InjectMocks放在被测试类上,且需要new被测试类

  2. @Mock放在外部依赖类上

  3. 对于被测试类的属性env,需要通过反射的方式赋值,写法繁琐,而Spock直接赋值即可

  4. 通过when-then的方式,模拟外部依赖类的行为结果,如when(taskRepository.getTask(1L)).thenReturn(task);

  5. 针对被测方法的多个逻辑分支,需要多个单测用例,才能全部覆盖,写法繁琐,而Spock只需要一个单测用例即可

public class TaskServiceTest {    /*** 被测试的类*/@InjectMocksprivate TaskService taskService = new TaskService();/*** 外部依赖的类*/@Mockprivate ITaskRepository taskRepository;@Beforepublic void before() {}/*** 测试日常环境的 查询任务** @throws IllegalAccessException 属性不可访问的异常* @throws NoSuchFieldException 没有属性的异常*/@Testpublic void testGetTaskDaily() throws IllegalAccessException, NoSuchFieldException {// 通过反射为环境变量赋值Field field = TaskService.class.getDeclaredField("env");field.setAccessible(true);field.set(taskService, EnvEnum.DAILY.getVal());Result<Task> result = taskService.getTask();Assert.assertTrue("日常环境测试失败", result.getData().getInput().contains(EnvEnum.DAILY.name()));}/*** 测试预发环境的 查询任务** @throws IllegalAccessException 属性不可访问的异常* @throws NoSuchFieldException 没有属性的异常*/@Testpublic void testGetTaskPre() throws IllegalAccessException, NoSuchFieldException {// 通过反射为环境变量赋值Field field = TaskService.class.getDeclaredField("env");field.setAccessible(true);field.set(taskService, EnvEnum.PRE.getVal());Result<Task> result = taskService.getTask();Assert.assertTrue("预发环境测试失败", result.getData().getInput().contains(EnvEnum.PRE.name()));}/*** 测试正式环境的 查询任务** @throws IllegalAccessException 属性不可访问的异常* @throws NoSuchFieldException 没有属性的异常*/@Testpublic void testGetTaskProduct() throws IllegalAccessException, NoSuchFieldException {// 通过反射为环境变量赋值Field field = TaskService.class.getDeclaredField("env");field.setAccessible(true);field.set(taskService, EnvEnum.PRODUCT.getVal());// 模拟 外部依赖方法的返回值Task task = new Task();task.setInput(EnvEnum.PRODUCT.name() + " 任务");when(taskRepository.getTask(1L)).thenReturn(task);Result<Task> result = taskService.getTask();Assert.assertTrue("正式环境测试失败", result.getData().getInput().contains(EnvEnum.PRODUCT.name()));}
}
  • Spock单测用例

如下是通过Spock写的getTask()方法的测试用例,可以看到只有1个测试用例,就能测试日常、预发、正式环境的逻辑,大致说下Spock的用法

  1. given块:用于写测试前的准备工作,例如我们只需要测试方法A的逻辑是否有问题,而方法A依赖外部类的方法B,那么可以在given块,模拟方法B的返回值

  2. expect块:用于写测试期望的结果,只能写判断式,如a==b,而参数a、b是在where块进行赋值的

  3. where块:用于写expect块断言的参数(测试数据、及期望返回值),where块可以写多组测试数据、和期望返回值,对于被测方法的逻辑有多个分支的情况,Spock的where特点,可以只写一次单测代码,就能模拟多组测试是否正确,大大节省写单测时间

class TaskServiceSpockTest extends Specification {/*** 模拟 外部依赖类*/ITaskRepository taskRepository = Mock()/*** 被测试类初始化*/TaskService taskService = new TaskService(taskRepository: taskRepository)void setup() {// 也可以在setup中,给TaskService的属性赋值// taskTestService.taskRepository = taskRepository}@Unrolldef "testGetTask 环境=#env, 任务包含关键字=#keyWord, 任务是否包含关键字=#result"() {given: "测试前的准备:给taskService的env赋值"taskService.env = envand: "mock taskRepository.getTask(_) 的返回值"Task task = new Task();task.setInput(EnvEnum.PRODUCT.name())taskRepository.getTask(_) >> taskand: "执行taskService.getTask()"Result<Task> taskResult = taskService.getTask()println(taskResult)expect: "expect只能写判断式,断言测试结果"result == taskResult.getData().getInput().contains(keyWord)where: "测试数据、及测试结果"env                      | keyWord                | resultEnvEnum.DAILY.getVal()   | EnvEnum.DAILY.name()   | trueEnvEnum.PRE.getVal()     | EnvEnum.PRE.name()     | trueEnvEnum.PRODUCT.getVal() | EnvEnum.PRODUCT.name() | true}
}

如下是Spock单测的执行结果,在结果中,可以清晰的看到入参、和对出参的断言是否正确。

  given-when-then

given-when-then组合常用于只需要一组测试数据的测试用例,其中,

  1. given块:用于写测试前的准备工作,例如我们只需要测试方法A的逻辑是否有问题,而方法A依赖外部类的方法B,那么可以在given块,模拟方法B的返回值

  2. when块:当被测方法的参数是什么的情况下,执行被测方法A

  3. then块:执行被测方法A后,会发生什么,可以断言依赖方法B执行的次数、抛出某种类型的异常、返回结果的断言等

仍以getTask()为例,介绍下given-when-then的用法

def "testGetTaskWhen"() {given: "测试前的准备: mock taskRepository.getTask(_)的返回值"Task task = new Task();task.setInput(EnvEnum.PRODUCT.name())taskRepository.getTask(_) >> taskand: "给taskService的env赋值"taskService.env = EnvEnum.PRODUCT.getVal()when: "执行被测试方法"Result<Task> result = taskService.getTask()println(result)then: "断言"// 断言:返回结果是trueresult.isSuccessful() == true// 断言:不会抛出异常noExceptionThrown()}

  模拟方法抛出异常

getTask有try-catch,那么怎么覆盖掉catch的逻辑呢?下面讲下,如何外部依赖方法抛出异常。

def "testGetTaskWhen 异常"() {given: "测试前的准备: mock taskRepository.getTask(_)抛出运行时异常"taskRepository.getTask(_) >> { throw new RuntimeException() }and: "给taskService的env赋值"taskService.env = EnvEnum.PRODUCT.getVal()when: "执行被测试方法"Result<Task> result1 = taskService.getTask()println(result1)then: "断言测试结果"result1.isSuccessful() == false}

  模拟方法每次的返回值不一样

在日常开发中,可能会遇到while查询某个方法,直到某种条件,才会break,如TaskService.getAllIntelligentConfigDTOList。为了测试这样的逻辑,就需要使每次mock方法的返回值不同。

如下,被测方法是获取全部任务的方法getAllTaskList(TaskQuery query),通过依赖外部的服务进行分页查询,直到全部查完。

public class TaskService {/*** 任务 管理类*/@Resourceprivate TaskManager taskManager;/*** 查询全部的任务信息** @param query 查询任务信息的query* @return 所有任务的集合*/public Result<List<TaskDTO>> getAllTaskList(TaskQuery query) {List<TaskDTO> allTaskList = Lists.newArrayList();// 查询全部的智能配置信息query.setPage(1);PageResult<TaskDTO> taskDTOPageResult = taskManager.queryList(query);while (taskDTOPageResult.isSuccessful()&& !CollectionUtils.isEmpty(taskDTOPageResult.getList())) {allTaskList.addAll(Lists.newArrayList(taskDTOPageResult.getList()));query.setPage(query.getPage() + 1);taskDTOPageResult = taskManager.queryList(query);}return Result.isOk(allTaskList);}
}

在写Spock单测的时候,只有第一次调用外部依赖的时候,返回非空集合,第二次调用的时候,返回空集合。

def "testGetAllIntelligentConfigDTOList"() {given: "测试前的准备"// 第一次调,返回 长度=1的集合IntelligentConfigDTO configDTO = new IntelligentConfigDTO();configDTO.setId(1L)com.alibaba.polystar.common.PageResult<IntelligentConfigDTO> pageResult =PageResult.build(1, 1, 1, Lists.newArrayList(configDTO))// 第二次调,返回 空集合,使while循环结束com.alibaba.polystar.common.PageResult<IntelligentConfigDTO> pageResult2 =PageResult.build(2, 1, 0, Lists.newArrayList())// 模拟方法调多次时,返回的结果configManager.queryList(_) >> pageResult >> pageResult2// 执行被测试方法IntelligentConfigQuery query = new IntelligentConfigQuery();Result<List<IntelligentConfigDTO>> result = taskService.getAllIntelligentConfigDTOList(query)println(result)// 智能配置的总条数def size = result.getData().size()expect: "expect只能是判断式:断言 测试结果,断言智能配置size=1"size == 1}

  模拟本类方法

在日常开发中,被测试方法A调用了同类的方法B,而B方法逻辑复杂,如getPreTask()方法,会调用本类的getInternalTask(),这时可以通过spy来mock本类方法getInternalTask(),来编写getPreTask()方法的单测。TaskService taskService = Spy()的作用是,如果TaskService的方法没有mock的话,则会执行方法;如果TaskService的方法被mock的话,则不会执行方法。这里的B方法有局限性,不能是私有方法,这时可以通过PowerMock进行模拟,可参考单元测试及框架简介(https://blog.csdn.net/luvinahlc/article/details/104427430)

def "testGetPreTask"() {given: "测试前的准备"// 通过spy创建TaskService,TaskService的方法如果没有mock的话,则会执行方法;如果TaskService的方法被mock的话,则不会执行方法TaskService taskService = Spy();and: "mock 本类的的方法"Task task = new Task();task.setInput("spy getInternalTask 任务");taskService.getInternalTask() >> taskand: "执行被测试方法"Result<Task> result = taskService.getPreTask()println(result)expect: "expect只能是判断式:断言测试结果"result.getData().getInput().contains("spy") == true}

  模拟静态方法

Spock可以兼容PowerMock,PowerMock支持模拟静态方法。如下StringCheckUtil.getLength(String string)是静态方法,StudentService.getStudentNameLength(String string)调用了静态方法。

public class StringCheckUtil {/*** 返回字符串长度** @param string 中英文混合的字符串* @return 0*/public static int getLength(String string) {return NumberUtils.INTEGER_MINUS_ONE;}
}
pulic StudentService {/*** 返回学生姓名长度** @param string 中英文混合的字符串* @return 字符串长度(中文占一个长度,2个英文占一个长度)*/public static int getStudentNameLength(String string) {return StringCheckUtil.getLength(string);}
}

Spock结合PowerMock模拟静态方法的用法如下

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([StringCheckUtil.class])
@SuppressStaticInitializationFor(["com.alibaba.polystar.common.util.StringCheckUtil"])
class StudentServiceSpockTest extends Specification {StudentService studentService = new StudentService()void setup() {// mock静态类PowerMockito.mockStatic(StringCheckUtil.class)}@Unrolldef "testGetStudentNameLength"() {given:PowerMockito.when(StringCheckUtil.getLength(Mockito.any())).thenReturn(6)when: "执行测试前的准备"int length = studentService.getStudentNameLength("小明")then: "断言"length == 2}
}

参考文献

  1. Spock单元测试框架介绍以及在美团优选的实践(https://tech.meituan.com/2021/08/06/spock-practice-in-meituan.html)

  2. Spock官网(https://spockframework.org/spock/docs/1.0/interaction_based_testing.html#_where_to_declare_interactions)

  3. 单元测试及框架简介(https://spockframework.org/spock/docs/1.0/interaction_based_testing.html#_where_to_declare_interactions)

总结

最后,总结一下Spock的特点:

  1. 支持模拟外部依赖方法,让测试重点关注代码逻辑的正确性

  2. 支持直接对被测类的属性赋值,而不必像Mockito那样通过反射为属性赋值

  3. 针对有多个逻辑分支的方法,只需要一个单测用例就能执行多组测试数据,而不必像Mockito需要多个单测用例

  4. Spock+PowerMock可以实现对静态方法的模拟

看到这里,是不是你也觉得Spock语法非常简洁、功能非常强大,那就快快使用起来吧

Spock单测利器的写法相关推荐

  1. 可能是全网最好的 Spock 单测入门文章!

    Spock 是非常简洁规范的单元测试框架,网上很多资料都不齐全,例子也很难懂.我自己经过一段时间的学习,梳理了这篇文章,不仅讲解层次递进,而且还有非常简洁明了的例子,小白都能懂! 快速入门 Spock ...

  2. 单测利器——PowerMockito使用心得

    PowerMockito是什么? PowerMockito是一个帮助我们快速写单测的工具,如果待测试方法中调用了其他方法或远程接口(如RPC.数据库等等),但是我们又不想在单测执行过程中真正的调用,而 ...

  3. Golang 高质量单测之 Table-Driven:从入门到真香

    一个开发人员,在不受外力胁迫的情况下 ,如何能自觉自愿写单测? 那必然是相信收益 > 成本.单测节省的未来修bug时间 > 写单测所花费的时间. 为了保证上述不等式成立,这边建议您考虑 t ...

  4. 【spock】单测竟然可以如此丝滑

    0. 为什么人人都讨厌写单测 在之前的关于swagger文章里提到过,程序员最讨厌的两件事,一件是别人不写文档,另一件就是自己写文档.这里如果把文档换成单元测试也同样成立. 每个开发人员都明白单元测试 ...

  5. 研效优化实践:Python单测——从入门到起飞

    作者:uniquewang,腾讯安全平台后台开发工程师 福生于微,积微成著,一行代码的精心调试,一条指令的细心验证,一个字节的研磨优化,都是影响企业研发效能工程的细节因素.而单元测试,是指针对软件中的 ...

  6. Jest + React Testing Library 单测总结

    大家好,我是若川.持续组织了6个月源码共读活动,感兴趣的可以点此加我微信 ruochuan02 参与,每周大家一起学习200行左右的源码,共同进步.同时极力推荐订阅我写的<学习源码整体架构系列& ...

  7. Spring boot Mybatis-Plus数据库单测实战(三种方式)

      单元测试长久以来是热门话题,本文不会讨论需不需要写单测,可以看看参考资料1,我个人认为写好单测应该是每个优秀开发者必备的技能,关于写单测的好处在这里我就不展开讨论了,快速进入本文着重讨论的话题,如 ...

  8. 百度单测生成技术如何召回线上服务的异常问题?

    导读:线上系统异常问题一直以来都是使人"闻风丧胆"的,传统手段在解决这类问题时面临着相应的技术瓶颈.基于此,探索基于单元测试召回异常问题的方法,实现了一套通用且无人参与的单测生成系 ...

  9. 从头到脚说单测——谈有效的单元测试

    导语 非常幸运的是,从4月份至今,我能够全身心投入到腾讯新闻的单元测试专项任务中,从无知懵懂,到不断深入理解的过程,与开发同学互帮互助,受益匪浅.在此过程中,得到了质量总监.新闻总监和乔帮主的倾囊指导 ...

最新文章

  1. Unity Shader 阴影
  2. 滑块验证码识别 java版本
  3. Qt 从C ++定义QML类型(二)
  4. python单词什么意思_“逐字逐句”是什么意思?语法在Python中意味着什么?
  5. 笨方法“学习python笔记之关键字
  6. 腾讯音乐计划以介绍形式在港交所主板二次上市
  7. 移动端触屏滑动touches使用
  8. MongoDB复制集全量同步改进
  9. python:将数据写入csv文件
  10. 最近点对问题C语言源代码
  11. 基于stm32f103c8t6的fft频率计
  12. 腾讯“鹅城”开建,斥资370亿;提前泄露小米新机博主被判赔偿100万;传字节跳动实习生删除GB以下所有机器学习模型 | EA周报...
  13. 微软云强劲增长的背后,是全新的人工智能黑科技
  14. Linux中设置开机启动执行命令和普通用户配置环境变量开机启动生效
  15. 【转】ASP六大对象介绍
  16. php x86什么意思,win10x86是什么意思
  17. LS1028GPIO初始化为高电平添加按键
  18. 双十一最后一天该怎么入手,几款必备的好物分享
  19. QT的基本使用(一):计算器界面的简易设计及其简单功能实现
  20. 鸿蒙和苹果拍照对比,iphone12mini和iphone12promax拍照对比:哪款手机拍照更好?

热门文章

  1. DSP TMS320F2803x、TMS320F2806x CLA开发笔记(代码基于TMS320F28069 详解)- 使用C语言编程CLA
  2. 设计模式-行为模式之Memento
  3. serialization的真情自述
  4. 超链接标签、注释标签
  5. 什么是弧幕投影以及特点在哪
  6. 语法糖是什么?(ES6的一些小笔记)
  7. 美国次级贷危机的普及版及概念解释
  8. 浏览器同源策略,跨域(跨源)
  9. 阿里云服务器购买以及搭建
  10. java —— 笨小猴