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



    前言

    相信看我博客的都是javaer,工作中一般都是使用Springboot框架
    之前介绍过,可以利用@Transactional注解实现单测方法回滚,其实大家都知道Springboot-Test里面集成了Mockito,今天我们来介绍下怎么使用,以及原理是什么。


    一、使用

    最佳实践

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = ApplicationLoader.class)
    public abstract class BaseTest {
    
        @SpyBean
        protected EcmsGateway ecmsGateway;
    
        // 注意这行,是数据库查询的mapper,需要配置name属性。
        @MockBean(name = "basicUserInfoPOExtMapper")
        protected BasicUserInfoPOExtMapper basicUserInfoPOExtMapper;
    
    }
    
        @Before
        @SneakyThrows //这个是lombok的注解,还是挺好用的
        public void init() {
    Mockito.doReturn(Collections.singletonList(userInfoPO))
                    .when(basicUserInfoPOExtMapper)
                    .listBasicUserInfo();
    Mockito.doReturn(Collections.singletonList(contractBaseInfo))
                            .when(ecmsGateway)
                    .queryMainContractList(Mockito.anyList());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    **注意mybatis的mapper必须使用@MockBean注解,且必须配置name属性。**但是也因此导致该mapper作用域下的所有方法如果没有指定返回范式,就会返回空对象。

    使用逻辑也很简单,就是通过MockBean和SpyBean对spring容器里注入的bean进行改造,然后在方法执行之前,通过Mockito的方法来指定当方法运行时,mock返回值。
    需要注意的是
    MockBean注解注入的属性,所有的方法调用,只要不指定返回范式和Mock对象,都是返回一个空的数据,null或者空的容器;而SpyBean注解注入的属性,默认方法调用还是走真实的链路调用,只有指定了返回范式的调用才会返回Mock的数据。

    使用场景

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

    @SpyBean失效场景

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

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    解决Mock失效的问题

    避免FactoryBean的实现方式

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

    @Repository
    public class BasicUserInfoRepository implements IBasicUserInfoRepository {
    
        @Resource
        private BasicUserInfoPOExtMapper basicUserInfoPOExtMapper;
    }
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringBootTest(classes = SpringBootStudyApplication.class)
    public class BaseTest {
        
        @SpyBean
        protected IBasicUserInfoRepository iBasicUserInfoRepository;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    使用@MockBean,但是要指定name

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringBootTest(classes = SpringBootStudyApplication.class)
    public class BaseTest {
    
        @MockBean(name = "basicUserInfoPOExtMapper")
        protected BasicUserInfoPOExtMapper basicUserInfoPOExtMapper;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    个人推荐

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

       @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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    二、原理

    1. @MockBean

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

    2.@SpyBean

    @SpyBean注解对于原本bean是通过FactoryBean添加到容器,且被proxy过的实例,是没法实现Mock的。
    @SpyBean注解的实现原理
    可以看到不管是@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
    
    • 1
    • 2

    方法调用

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


    总结

    1. 文章主要讲了Springboot中的@MockBean和@SpyBean的使用场景和简单原理。
    2. 大家使用数据库一般都是通过spring-mybatis将mapper注入到spring容器的,导致使用@SpyBean不会生效,推荐使用@MockBean,同时指定name属性来实现数据库查询的返回Mock。当然笔者更推荐大家使用@Transactional注解实现回滚来达到数据库查询结果的设置。
  • 相关阅读:
    MindSpore GPU版本安装指导
    贪心算法总结篇
    Compose Canvas饼图效果绘制
    openai sora 只能根据文本生成视频?不,TA 是通用物理世界模拟器
    六:ffmpe音频参数的使用
    Mac电脑VSCode配置PHP开发环境
    MySQL数据库管理及用户管理以及数据库用户授权
    第二章《Java程序世界初探》第1节:认识Java语言的变量
    JVM之运行时数据区 面试相关
    索尼PS VR2体验:硬件素质不错,高质量游戏是关键
  • 原文地址:https://blog.csdn.net/liangsheng_g/article/details/133843160