网易首页 > 网易号 > 正文 申请入驻

Spock 单测利器的写法

0
分享至

作 者 | 理莎

文章来源 | 阿里巴巴淘系技术

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包


org.codehaus.groovygroupId>
groovy-all-testsartifactId>
2.0.0-rc-3version>
dependency>


org.spockframeworkgroupId>
spock-coreartifactId>
1.3-groovy-2.4version>
dependency>
org.spockframeworkgroupId>
spock-springartifactId>
1.3-groovy-2.4version>
testscope>
dependency>

配置插件

org.codehaus.gmavenplusgroupId>
gmavenplus-pluginartifactId>
1.4version>
trueextensions>
compilegoal>
testCompilegoal>
goals>
execution>
executions>


${project.basedir}/src/test/javadirectory>
**/*.groovyinclude>
includes>
testSource>
${project.basedir}/src/test/groovydirectory>
**/*.groovyinclude>
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;

/**
* 任务 服务类
*/
@Resource
private ITaskRepository taskRepository;

/**
* 查询任务信息(根据环境,查询任务)
*
* @return 任务
*/
public Result getTask() {
if (EnvEnum.isDaily(env)) {
// 日常环境,任务取值 本方法直接new
Task 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 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 {
/**
* 被测试的类
*/
@InjectMocks
private TaskService taskService = new TaskService();

/**
* 外部依赖的类
*/
@Mock
private ITaskRepository taskRepository;

@Before
public void before() {
}

/**
* 测试日常环境的 查询任务
*
* @throws IllegalAccessException 属性不可访问的异常
* @throws NoSuchFieldException 没有属性的异常
*/
@Test
public void testGetTaskDaily() throws IllegalAccessException, NoSuchFieldException {
// 通过反射为环境变量赋值
Field field = TaskService.class.getDeclaredField("env");
field.setAccessible(true);
field.set(taskService, EnvEnum.DAILY.getVal());

Result result = taskService.getTask();
Assert.assertTrue("日常环境测试失败", result.getData().getInput().contains(EnvEnum.DAILY.name()));
}

/**
* 测试预发环境的 查询任务
*
* @throws IllegalAccessException 属性不可访问的异常
* @throws NoSuchFieldException 没有属性的异常
*/
@Test
public void testGetTaskPre() throws IllegalAccessException, NoSuchFieldException {
// 通过反射为环境变量赋值
Field field = TaskService.class.getDeclaredField("env");
field.setAccessible(true);
field.set(taskService, EnvEnum.PRE.getVal());

Result result = taskService.getTask();
Assert.assertTrue("预发环境测试失败", result.getData().getInput().contains(EnvEnum.PRE.name()));
}

/**
* 测试正式环境的 查询任务
*
* @throws IllegalAccessException 属性不可访问的异常
* @throws NoSuchFieldException 没有属性的异常
*/
@Test
public 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 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
}

@Unroll
def "testGetTask 环境=#env, 任务包含关键字=#keyWord, 任务是否包含关键字=#result"() {
given: "测试前的准备:给taskService的env赋值"
taskService.env = env

and: "mock taskRepository.getTask(_) 的返回值"
Task task = new Task();
task.setInput(EnvEnum.PRODUCT.name())
taskRepository.getTask(_) >> task

and: "执行taskService.getTask()"
Result taskResult = taskService.getTask()
println(taskResult)

expect: "expect只能写判断式,断言测试结果"
result == taskResult.getData().getInput().contains(keyWord)

where: "测试数据、及测试结果"
env | keyWord | result
EnvEnum.DAILY.getVal() | EnvEnum.DAILY.name() | true
EnvEnum.PRE.getVal() | EnvEnum.PRE.name() | true
EnvEnum.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(_) >> task

and: "给taskService的env赋值"
taskService.env = EnvEnum.PRODUCT.getVal()

when: "执行被测试方法"
Result result = taskService.getTask()
println(result)

then: "断言"
// 断言:返回结果是true
result.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 result1 = taskService.getTask()
println(result1)

then: "断言测试结果"
result1.isSuccessful() == false
}

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

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

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

public class TaskService {
/**
* 任务 管理类
*/
@Resource
private TaskManager taskManager;

/**
* 查询全部的任务信息
*
* @param query 查询任务信息的query
* @return 所有任务的集合
*/
public Result getAllTaskList(TaskQuery query) {
List allTaskList = Lists.newArrayList();

// 查询全部的智能配置信息
query.setPage(1);
PageResult 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 pageResult =
PageResult.build(1, 1, 1, Lists.newArrayList(configDTO))

// 第二次调,返回 空集合,使while循环结束
com.alibaba.polystar.common.PageResult pageResult2 =
PageResult.build(2, 1, 0, Lists.newArrayList())

// 模拟方法调多次时,返回的结果
configManager.queryList(_) >> pageResult >> pageResult2

// 执行被测试方法
IntelligentConfigQuery query = new IntelligentConfigQuery();
Result 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() >> task

and: "执行被测试方法"
Result 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)
}

@Unroll
def "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语法非常简洁、功能非常强大,那就快快使用起来吧。

云栖号的伙伴群开启了,欢迎大家入群聊起来!

大家想看什么内容,我们可以一起聊聊~

✨ 精彩推荐✨

最 新 活 动

技 术 好 文

企 业 案 例

↓ 直通2022年采购季主会场!

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
冒充职业球员?欧洲杯惊现200多斤大胖子 当年差点加盟中超

冒充职业球员?欧洲杯惊现200多斤大胖子 当年差点加盟中超

球事百科吖
2024-06-16 00:04:33
倒查30年后补税是个危险信号

倒查30年后补税是个危险信号

深度财线
2024-06-15 22:03:47
革命性突破!美企成功研制核电池,几乎无辐射,电量够用100年

革命性突破!美企成功研制核电池,几乎无辐射,电量够用100年

十三级台阶
2024-06-15 15:51:27
“复兴号”列车车厢转运过程中被货车撞上,交警:无人受伤

“复兴号”列车车厢转运过程中被货车撞上,交警:无人受伤

极目新闻
2024-06-15 22:36:25
五大名医集体总结:增强身体健康的10大铁律,值得参考~

五大名医集体总结:增强身体健康的10大铁律,值得参考~

华人星光
2024-06-15 16:49:27
4人被查,3人被处分……

4人被查,3人被处分……

黄河新闻网吕梁频道
2024-06-15 08:41:51
日本3-0塞尔维亚获8.68分:中国女排亚洲第1危矣 朱婷腰伤成X因素

日本3-0塞尔维亚获8.68分:中国女排亚洲第1危矣 朱婷腰伤成X因素

颜小白的篮球梦
2024-06-15 19:57:18
中俄联合开发黑瞎子岛,当年黑瞎子岛是怎样被俄方占领的?

中俄联合开发黑瞎子岛,当年黑瞎子岛是怎样被俄方占领的?

浩然史观
2024-06-15 16:55:02
大陆不再沉默,给黄仁勋上了一课,选在美收紧对华AI芯片出口之际

大陆不再沉默,给黄仁勋上了一课,选在美收紧对华AI芯片出口之际

陈菲副教授
2024-06-15 18:20:03
陪睡门曝光!双一流大学女生陷陪睡丑闻,交友App成道德沦丧背后推手?

陪睡门曝光!双一流大学女生陷陪睡丑闻,交友App成道德沦丧背后推手?

新青年大院NEWYOUTH
2024-06-16 00:06:48
欧洲杯高质量3-1!前国脚董方卓感慨:中国足球已跟不上时代步伐

欧洲杯高质量3-1!前国脚董方卓感慨:中国足球已跟不上时代步伐

黑色柳丁
2024-06-15 23:51:10
网传南方医科大学老师为抢救患儿迟到29分钟,被举报扣款2000元?

网传南方医科大学老师为抢救患儿迟到29分钟,被举报扣款2000元?

火山诗话
2024-06-16 06:51:58
一针69.7万,医保谈判到3万!可澳洲184块,为何却说中国价最低?

一针69.7万,医保谈判到3万!可澳洲184块,为何却说中国价最低?

风起云间
2024-06-14 21:44:22
女性私处的“芳草”,竟然是越“浓密”越“渴望”?

女性私处的“芳草”,竟然是越“浓密”越“渴望”?

水白头
2024-06-16 00:06:07
吕迪格回应定妆照争议手势:很多球员摆出这个姿势,没有宗教意涵

吕迪格回应定妆照争议手势:很多球员摆出这个姿势,没有宗教意涵

直播吧
2024-06-16 06:45:07
0-2到3-2!女排绝地反击,逆转世界第1,日本队空欢喜一场!

0-2到3-2!女排绝地反击,逆转世界第1,日本队空欢喜一场!

钉钉陌上花开
2024-06-15 22:43:32
欧洲杯被中文广告包围?破案了:均为虚拟广告牌,与现场不一样!

欧洲杯被中文广告包围?破案了:均为虚拟广告牌,与现场不一样!

风过乡
2024-06-15 16:53:50
31岁河南禹州市医保局局长任小龙,拟遴选进入省政府办公厅

31岁河南禹州市医保局局长任小龙,拟遴选进入省政府办公厅

澎湃新闻
2024-06-15 21:58:28
税太高了?一家企业要交13%增值税、25%企业所得税、20%分红税

税太高了?一家企业要交13%增值税、25%企业所得税、20%分红税

小蜜情感说
2024-06-16 07:50:02
追梦回应克莱取关勇士:我觉得这很好笑 你们都想看有人感到受伤

追梦回应克莱取关勇士:我觉得这很好笑 你们都想看有人感到受伤

罗说NBA
2024-06-16 05:19:26
2024-06-16 11:16:49
阿里云云栖号
阿里云云栖号
阿里云官方内容社区!
2941文章数 864关注度
往期回顾 全部

科技要闻

iPhone 16会杀死大模型APP吗?

头条要闻

40余套房屋涉嫌"一房多卖" 有购房者内心积郁因病去世

头条要闻

40余套房屋涉嫌"一房多卖" 有购房者内心积郁因病去世

体育要闻

没人永远年轻 但青春如此无敌还是离谱了些

娱乐要闻

江宏杰秀儿女刺青,不怕刺激福原爱?

财经要闻

打断妻子多根肋骨 上市公司创始人被公诉

汽车要闻

东风奕派eπ008售21.66万元 冰箱彩电都配齐

态度原创

时尚
教育
本地
数码
房产

中年女性还是穿连衣裙最有气质!裙摆过膝、腰部收紧,巨显瘦

教育要闻

计算机专业,会是下一个土木吗?

本地新闻

粽情一夏|海河龙舟赛,竟然成了外国人的大party!

数码要闻

小米 Redmi Buds 6 青春版耳机通过多项认证,搭载恒玄蓝牙方案

房产要闻

万华对面!海口今年首宗超百亩宅地,重磅挂出!

无障碍浏览 进入关怀版