Springboot结合Mockito写单元测试实践和原理

文章目录


前言

相信看我博客的都是javaer,工作中一般都是使用Springboot框架。

之前介绍过,可以利用@Transactional注解实现单测方法回滚,其实大家都知道Springboot-Test里面集成了Mockito,今天我们来介绍下怎么使用,以及原理是什么。


一、使用

最佳实践

java 复制代码
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationLoader.class)
public abstract class BaseTest {

    @SpyBean
    protected EcmsGateway ecmsGateway;

    @MockBean(name = "purchaseActivityTService")
    protected PurchaseActivityTService.Iface purchaseActivityTService;

}

    @Before
    @SneakyThrows //这个是lombok的注解,还是挺好用的
    public void init() {
Mockito.doReturn(mockResponse).when(purchaseActivityTService).createActivity(Mockito.any(TPurchaseActivityInfo.class), Mockito.anyLong());
Mockito.doReturn(Collections.singletonList(contractBaseInfo))
                        .when(ecmsGateway)
                .queryMainContractList(Mockito.anyList());
    }

使用逻辑也很简单,就是通过MockBean和SpyBean对spring容器里注入的bean进行改造,然后在方法执行之前,通过Mockito的方法来指定当方法运行时,mock返回值。

需要注意的是

MockBean注解注入的属性,所有的方法调用,只要不指定返回范式和Mock对象,都是返回一个空的数据,null或者空的容器;而SpyBean注解注入的属性,默认方法调用还是走真实的链路调用,只有指定了返回范式的调用才会返回Mock的数据。

使用场景

一般情况下,咱们需要mock数据的场景是对于RPC调用,或者数据库查询(当然这个其实也是一种RPC)调用。
需要注意,很多同学发现Mock数据库查询会失效,具体原因和下面的是同一种情况!!!

@SpyBean失效场景

通过FactoryBean返回的对象类型是一个proxy时

java 复制代码
Caused by: org.mockito.exceptions.base.MockitoException: 
Cannot mock/spy class com.sun.proxy.$Proxy146
注意这句报错!!!!!!
Mockito cannot mock/spy because :
 - final class
	at org.springframework.boot.test.mock.mockito.SpyDefinition.createSpy(SpyDefinition.java:103)
	at org.springframework.boot.test.mock.mockito.MockitoPostProcessor.createSpyIfNecessary(MockitoPostProcessor.java:358)
	at org.springframework.boot.test.mock.mockito.MockitoPostProcessor$SpyPostProcessor.createSpyIfNecessary(MockitoPostProcessor.java:496)
	at org.springframework.boot.test.mock.mockito.MockitoPostProcessor$SpyPostProcessor.postProcessAfterInitialization(MockitoPostProcessor.java:492)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:431)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.postProcessObjectFromFactoryBean(AbstractAutowireCapableBeanFactory.java:1836)
	at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.getObjectFromFactoryBean(FactoryBeanRegistrySupport.java:116)
	... 106 more

解决Mock失效的问题

避免FactoryBean的实现方式

目前mybatis针对poMapper的实现都是通过MapperFactoryBean实现的,所以,最简单的方式,可以基于mapper封装repository层,然后对repository层的对象添加@MockBean或者@SpyBean,进行调用。

譬如:

java 复制代码
@Repository
public class BasicUserInfoRepository implements IBasicUserInfoRepository {

    @Resource
    private BasicUserInfoPOExtMapper basicUserInfoPOExtMapper;
}

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = SpringBootStudyApplication.class)
public class BaseTest {
    
    @SpyBean
    protected IBasicUserInfoRepository iBasicUserInfoRepository;

}

使用@MockBean,但是要指定name

java 复制代码
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = SpringBootStudyApplication.class)
public class BaseTest {

    @MockBean(name = "basicUserInfoPOExtMapper")
    protected BasicUserInfoPOExtMapper basicUserInfoPOExtMapper;

}

个人推荐

其实针对查询数据库的场景,个人还是推荐通过@Transactional注解达到数据回滚来实现查询数据的诉求,毕竟要是通过对poMapper添加@MockBean使得数据库查询能够Mock,但是由此导致了该poMapper的所有方法都需要指定返回范式。实例如下:

java 复制代码
   @Transactional
   @Test
   public void testReceive() {
       // 前置将任务状态修改
       Long jobId = 255L;
       SePriceJob priceJob = new SePriceJob();
       priceJob.setId(jobId);
       priceJob.setAcceptType(JobAcceptType.CREATE_SUPPLIER_MAKE_UP.getAcceptType());
       priceJob.setStatus(PriceJobStatus.DONE.getStatus());
       priceJob.setTaskStatus(PriceJobTaskStatus.PROCESSING.getStatus());
       priceJobMapper.updateByPrimaryKeySelective(priceJob, SePriceJob.Column.acceptType, SePriceJob.Column.status, SePriceJob.Column.taskStatus);
       List<SePriceJob> priceJobList = priceJobMapper.selectByExample(SePriceJobExample.newAndCreateCriteria().andIdEqualTo(jobId).example());
       assert priceJobList.get(0).getTaskStatus().equals(PriceJobTaskStatus.PROCESSING.getStatus());
   }

二、原理

1. @MockBean

需要注意的是**@MockBean有时候需要指定name属性**,否则默认注入到Spring容器中的对象beanName是类的全限定名,导致其他bean在注入的时候获取到的不是该mockBean。

2.@SpyBean

@SpyBean注解对于原本bean是通过FactoryBean添加到容器,且被proxy过的实例,是没法实现Mock的。

可以看到不管是@MockBean还是@SpyBean,都是给spring的bean创建增加一个MockMethodInterceptor。

区别在于

复制代码
1. @MockBean创建的mock对象是直接new出来的,而且只有MockMethodInterceptor这一个advisor;而@SpyBean是通过给spring容器创建的对象增加一个advisor:MockMethodInterceptor
2. 那MockMethodInterceptor在执行的时候是怎么区分当前对象到底是@MockBean还是@SpyBean修饰呢?MockMethodInterceptor对象有个属性MockCreationSettings mockCreationSettings,它有个属性defaultAnswer。@MockBean指定了该属性为Answers.RETURNS_DEFAULTS,而@SpyBean通过MockitoPostProcessor的createSpyIfNecessary,给指定的取值是Mockito.CALLS_REAL_METHODS

方法调用

方法调用的源码就不贴了,简单来说就是给MockMethodInterceptor实例增加一个返回范式,譬如创建一个matcher,指定该matcher命中时,返回某个对象;实际运行时,通过匹配matcher来决定是否返回Mock结果。


总结

  1. 文章主要讲了Springboot中的@MockBean和@SpyBean的使用场景和简单原理。
  2. 大家使用数据库一般都是通过spring-mybatis将mapper注入到spring容器的,导致使用@SpyBean不会生效,推荐使用@MockBean,同时指定name属性来实现数据库查询的返回Mock。当然笔者更推荐大家使用@Transactional注解实现回滚来达到数据库查询结果的设置。
相关推荐
贫民窟的勇敢爷们4 小时前
SpringBoot整合AOP切面编程实战,实现日志统一记录+接口权限校验
java·spring boot·spring
吾疾唯君医8 小时前
Java SpringBoot集成积木报表实操记录
java·spring boot·spring·导出excel·积木报表·数据文件下载
正儿八经的少年11 小时前
Spring Boot 两种激活配置方式的作用与区别
java·spring boot·后端
疯狂成瘾者12 小时前
Spring Boot 项目中的 SMTP 邮件验证码服务技术解析
java·spring boot·后端
啃臭13 小时前
AOP和反射
java·spring boot
河阿里13 小时前
SpringBoot:Spring Task定时任务完整使用教学
java·spring boot·spring
五阿哥永琪16 小时前
从0开始做一个导出功能,完整流程
spring boot
java1234_小锋17 小时前
SpringBoot可以同时处理多少请求?
java·spring boot·后端
海棠Flower未眠17 小时前
Spring Boot 3 + JPA多模块系统对MySQL和DORIS进行多数据源集成实战(荣耀典藏版)
spring boot·后端·mysql
北风朝向18 小时前
Spring Boot 集成 Open WebUI 实现 AI 流式对话
人工智能·spring boot·状态模式