优雅单测-3用Mockito轻松解决复杂的依赖问题
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了优雅单测-3用Mockito轻松解决复杂的依赖问题相关的知识,希望对你有一定的参考价值。
参考技术AMockito是GitHub上用于Java使用最广泛的Mock框架
Mockito核心解决的问题:不管是测试驱动开发,还是系统架构本身,都会带来的依赖结构复杂问题;单元测试做软件中的最小可测试单元,只应依当前单元的配置。 所有较大的系统都会有依赖复杂的问题。 此类问题就需要Mock框架来解决
使用很简单一般分3部:
@MockBean,在SpringBoot中通过@MockBean注解,就可以快速装在一个Mock的Bean了。
此方式通过非常简单的方式解决了系统层级之间单测的依赖,可以用在数据层和业务层,也可以用在业务层之间的多项依赖等依赖上的问题
直接执行:
可以看到如果使用了SpringBoot,并且使用@MockBean可以快速解决依赖的问题。但是,Mock出的LiveReportMapper对象其实是个空壳,那么既然是mock就要知道如何通过mock的方式解决各种复杂逻辑
下面模拟2种复杂逻辑,Service层的单元测试mock依赖的另外一个Service类"InfoService" ,并且mock里面的2个方法
下面看例子:
如果没有SpringBoot环境的Starter,可以直接使用Mockito。 除了@MockBean不能使用其他功能不受影响,参考上面例子
更多用法参考Mockito官方文档,此处不做赘述
官网地址: https://site.mockito.org/
Spock单测利器的写法
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)单测框架的对比:
JUnit适用于没有外部依赖服务、或者外部依赖服务较少的简单类的单测,对于有外部依赖服务的类、或者对运行环境有要求的类,Junit模拟外部依赖、环境非常耗时。
Mock类型的单测方式会解决外部依赖不容易模拟的问题,常见的Mock有Jmock、Mockito、PowerMock、Spock等,简单对比下几种Mock:
Jmock:通过模拟外部依赖对象来模拟其的行为,从而隔离不关心的外部依赖对象,使单测专注于被测方法的逻辑是否正确。
Mockito:Mockito是Jmock的升级版,Jmock需要在执行前记录期望的行为,而Mockito只需要在执行后校验哪些函数被调用即可,写法更干净、简洁。
PowerMock:PowerMock是在Mockito的基础上,又支持了对静态方法、私有方法、构造函数的模拟。但是由于PowerMock会篡改字节码,导致测试时的字节码与编译出来的字节码不同,而单测的覆盖率大多是基于字节码统计的,导致PowerMock编写的单测不能被统计进覆盖率,这是PowerMock的硬伤。
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组合常用于被测方法包含多个逻辑分支的测试,其中,
given块:用于写测试前的准备工作,例如我们只需要测试方法A的逻辑是否有问题,而方法A依赖外部类的方法B,那么可以在given块,模拟方法B的返回值
expect块:用于写测试期望的结果,只能写判断式,如a==b,而参数a、b是在where块进行赋值的
where块:用于写expect块断言的参数(测试数据、及期望返回值),where块可以写多组测试数据、和期望返回值,对于被测方法的逻辑有多个分支的情况,Spock的where特点,可以只写一次单测代码,就能模拟多组测试是否正确,大大节省写单测时间
如下是被测方法TaskService.getTask(),不同的系统环境,获取到的任务也不同,为了确保各个环境查询任务的正确性,需要覆盖所有分支。
public class TaskService
/**
* 环境
*/
@Value("$spring.current.env")
private String env;
/**
* 任务 服务类
*/
@Resource
private ITaskRepository taskRepository;
/**
* 查询任务信息(根据环境,查询任务)
*
* @return 任务
*/
public Result<Task> 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<Task> getPreTask()
Task task = new Task();
task.setInput("TaskService getInternalTask 任务" + EnvEnum.PRE.name());
return Result.isOk(task);
Mockito单测用例
如下是通过Mockito写的getTask()方法的测试用例,可以看到一共有3个测试用例,分别是日常、预发、正式环境,我们以正式环境的测试用例为例说下Mockito的用法
@InjectMocks放在被测试类上,且需要new被测试类
@Mock放在外部依赖类上
对于被测试类的属性env,需要通过反射的方式赋值,写法繁琐,而Spock直接赋值即可
通过when-then的方式,模拟外部依赖类的行为结果,如when(taskRepository.getTask(1L)).thenReturn(task);
针对被测方法的多个逻辑分支,需要多个单测用例,才能全部覆盖,写法繁琐,而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<Task> 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<Task> 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<Task> result = taskService.getTask();
Assert.assertTrue("正式环境测试失败", result.getData().getInput().contains(EnvEnum.PRODUCT.name()));
Spock单测用例
如下是通过Spock写的getTask()方法的测试用例,可以看到只有1个测试用例,就能测试日常、预发、正式环境的逻辑,大致说下Spock的用法
given块:用于写测试前的准备工作,例如我们只需要测试方法A的逻辑是否有问题,而方法A依赖外部类的方法B,那么可以在given块,模拟方法B的返回值
expect块:用于写测试期望的结果,只能写判断式,如a==b,而参数a、b是在where块进行赋值的
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<Task> 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组合常用于只需要一组测试数据的测试用例,其中,
given块:用于写测试前的准备工作,例如我们只需要测试方法A的逻辑是否有问题,而方法A依赖外部类的方法B,那么可以在given块,模拟方法B的返回值
when块:当被测方法的参数是什么的情况下,执行被测方法A
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<Task> 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<Task> 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<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() >> task
and: "执行被测试方法"
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)
@Unroll
def "testGetStudentNameLength"()
given:
PowerMockito.when(StringCheckUtil.getLength(Mockito.any())).thenReturn(6)
when: "执行测试前的准备"
int length = studentService.getStudentNameLength("小明")
then: "断言"
length == 2
参考文献
Spock单元测试框架介绍以及在美团优选的实践(https://tech.meituan.com/2021/08/06/spock-practice-in-meituan.html)
Spock官网(https://spockframework.org/spock/docs/1.0/interaction_based_testing.html#_where_to_declare_interactions)
单元测试及框架简介(https://spockframework.org/spock/docs/1.0/interaction_based_testing.html#_where_to_declare_interactions)
总结
最后,总结一下Spock的特点:
支持模拟外部依赖方法,让测试重点关注代码逻辑的正确性
支持直接对被测类的属性赋值,而不必像Mockito那样通过反射为属性赋值
针对有多个逻辑分支的方法,只需要一个单测用例就能执行多组测试数据,而不必像Mockito需要多个单测用例
Spock+PowerMock可以实现对静态方法的模拟
看到这里,是不是你也觉得Spock语法非常简洁、功能非常强大,那就快快使用起来吧😄。
团队介绍
大淘宝技术-用户增长团队
用户增长团队是一支研发、数据、算法一体的团队,在对用户争夺进入白热化的时期,用户增长团队正承担着捍卫电商主板块增长的重要使命,我们将在最贴近用户的前线战场,用持续的技术创新来驱动阿里电商巨擎的稳步前行。
这是一支年轻开放的团队,倡导“增长黑客”极客氛围,在这里你将收获超大规模高并发的架构能力,洞悉用户增长最前沿的实践方法,在数字化时代具备最核心的竞争力。团队技术氛围浓厚,研发流程规范,代码质量高,学习成长速度快。
如果您有兴趣可将简历发至jingran.ljr@alibaba-inc.com,期待您的加入。PS 只面向2023年毕业的校招实习生童鞋。
✿ 拓展阅读
作者|理莎
编辑|橙子君
出品|阿里巴巴新零售淘系技术
以上是关于优雅单测-3用Mockito轻松解决复杂的依赖问题的主要内容,如果未能解决你的问题,请参考以下文章