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注解实现回滚来达到数据库查询结果的设置。
相关推荐
iPadiPhone2 分钟前
Spring Boot 核心注解全维度解析与面试复盘
java·spring boot·后端·spring·面试
彭于晏Yan7 分钟前
Spring Cloud Stream使用
spring boot·后端·spring cloud
常利兵12 分钟前
Kotlin 延迟初始化:lateinit与by lazy的华山论剑
spring boot·后端·状态模式
沙雕不是雕又菜又爱玩16 分钟前
基于springboot的超市收银系统
java·spring boot·intellij-idea
l软件定制开发工作室22 分钟前
Spring开发系列教程(32)——Spring Boot开发
java·spring boot·后端·spring
彭于晏Yan30 分钟前
JsonProperty注解的access属性
java·spring boot
醇氧38 分钟前
PowerPoint 批量转换为 PDF
java·spring boot·spring·pdf·powerpoint
gxy1990261 小时前
【springboot】Spring 官方抛弃了 Java 8!新idea如何创建java8项目
java·spring boot·spring
Chan161 小时前
双非 Java 后端首次实习 | 个人经验分享总结
java·开发语言·spring boot·spring·java-ee·intellij-idea
HalvmånEver10 小时前
7.高并发内存池大页内存申请释放以及使用定长内存池脱离new
java·spring boot·spring