zoukankan      html  css  js  c++  java
  • 单元测试中简单使用Mockito解决Spring Bean依赖树问题

    前提

    本文不是针对Mockito的入门教学 ,主要叙述如何简单的使用Mockito解决Bean依赖树问题,对于Mockito的学习请找其他的文章或者查阅官方文档

    基本概念

    Junit初始化及存在的问题

    spring应用在unit test时,test是独立运行的,所以需要自行 init ApplicationContext,启动 Ioc容器。

    Junit要求:Test类中涉及的所有Spring bean 注入成功才能完成applicationContext初始化,并启动IOC容器,否则无法执行unit test。

    ApplicationContext初始化的两种方式

    1. 手动注入(使用 @Bean或者 @Component 注入所需的类)
    2. 编写@Configuration 类(使用@ComponentScan 指定扫描beans)

    两种初始化方式存在的问题

    方式一:

    • 所需的beans中,一个bean少注入了就会导致无法初始化上下文
    • 需要注入的bean太多时,需要花费大量的时间和精力,排查缺漏难度大

    方式二:

    • 颗粒度难以把控,随着项目规模变大之后,可能导致bean导入过多,单元测试跑很久才能通过
    • 当项目规模大了之后,bean之间的依赖往往是复杂的,扫描bean的方式可能出现一些不属于自己模块的未知问题或者某些中间件在unitTest环境无法正常启动,导致无法初始化上下文

    什么是依赖树?

    img

    在开发应用时,往往会出现如上图的 树型依赖 ,比如 serviceA 调用 serviceB,serviceB 又调用 serviceC 。

    然而这只是一个简单的例子。真正的开发中,往往一个 service 会依赖多个 service ,以及多个 dao ,以此来实现业务逻辑。

    而根据Junit要求,我们必须将树的路径经过的所有节点(bean)都注入才能完成spring上下文初始化。这时如果bean之间的依赖耦合过大时,就无法跳脱出两种初始化方式带来的问题。

    什么是Mockito?

    在测试过程中,对于某些不容易构造(如 HttpServletRequest 必须在Servlet 容器中才能构造出来)或者不容易获取比较复杂的对象(如 JDBC 中的ResultSet 对象),用一个虚拟对象(Mock 对象)来创建以便测试的测试方法。

    Mock 最大的功能是帮你把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为。

    简单来说:就是虚拟一个mock对象,这个对象在单元测试时会“狸猫换太子”,将原有bean进行替换,“骗过”spring初始化,成功启动ioc容器,以此规避常规初始化方式带来的种种问题。

    开发场景

    结合本人在工作中遇见的问题,当时我所写的模块进行unitTest时,就出现了依赖树过于庞大的问题。

    1. 首先,我采用了常规的手动注入(方式一),导致注入了很久都没注入完,无法执行测试。后来觉得这方法在这种情况不可行。
    2. 然后,我采用了编写@Configuration 类(方式二),同样也存在一些问题。一些不属于我负责模块的bean也被注入,其中某些涉及TaskSchedule的bean无法被正确注入,导致无法执行测试。此时一个个bean探索,解决问题显然不现实。
    3. 最后,我采用Junit+Mockito结合的方式进行单元测试。按照依赖树大小进行区分。
      • 依赖树小的直接使用常规的手动注入(方式一),省事,同时保证大部分逻辑按照代码正常运行
      • 依赖树大的使用Mockito,避免前文提到的两种初始化方式导致的问题

    #使用

    1 导入maven依赖

    首先导入mockito maven依赖,版本请根据自己的spring版本选择,否则会出现不兼容的情况。

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintage</groupId>
                        <artifactId>junit-vintage-engine</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
                <scope>test</scope>
            </dependency>
    

    注意:

    • 此处导入了spring-boot-starter-test是因为这个依赖已经包含了mockito相关的jar包

    • spring-boot-starter-test可以使用 @MockBean 注解(mockito-core、mockito-all貌似不能)

    @Mock和@MockBean的区别:

    @Mock @MockBean
    mock bean替换原有bean时机 spring上下文初始化 完成之后 spring上下文初始化 执行期间
    能否“骗”过spring初始化
    能否解决依赖树
    1. 在没注入所有所需的bean,无法完成spring上下文初始化时,@Mock无法正常工作
    2. @MockBean在初始化时就进行替换,spring上下文初始化时检测的bean为替换后的mock bean,而mock bean本身是无依赖任何其他bean的,自然能够“骗”过spring上下文初始化阶段,成功启动IOC容器

    2 分析bean之间的依赖

    使用一个简单的Demo进行开发场景的模拟,采用Junit+Mockito结合的方式进行单元测试,根据依赖树大小区分出是否需要mock

    如图,此处编写了一个ControllerA,ControllerA中依赖了2个bean:ServiceA,DaoA

    分析过程:
    1. 关于 DaoA :由于Dao往往不会依赖其他的bean,所以此处可以使用常规的手动注入(方式一)即可。方便快捷
    2. 关于 ServiceA :由于serviceA依赖了serviceB(->DaoB)、serviceC(->DaoC),像这样的嵌套依赖的bean就可以使用Mockito,来解决依赖树问题

    3 编写Test类


    daoA使用@Bean注解注入即可

            @Bean
            public DaoA daoA(){
                return new DaoAImpl();
            }
    

    1.serviceA首先使用@MockBean注解,将serviceA模拟为Mock Bean,它将在spring上下文初始化时就替换掉原有Bean

        @MockBean
        private ServiceA serviceA;
    

    2.在test类执行前(@Before),使用Mockito API设置调用某个方法的返回值(你预期得到的返回结果),在Test类中调用这个方法时就会返回所指定的值

    @Before
        public void init(){
            MockitoAnnotations.initMocks(this);//只使用 @MockBean 时可省略这句
            when(controllerA.serviceA_method()).thenReturn("666");
        }
    

    3.使用 @InjectMocks 通知依赖了serviceA的controllerA,在spring启动时,对controllerA这个bean进行相应的后置处理

        @Autowired
        @InjectMocks
        private ControllerA controllerA;
    

    4.单元测试时,就不会使用原有Bean的方法,而是使用Mock Bean及其已经指定了返回值的方法

        @Test
        public void testDeepMock() {
            String s = controllerA.serviceA_method();
            System.out.println(s);
        }
    

    5.unitTest结果

  • 相关阅读:
    朱刘算法---有向图的最小生成树
    527D Clique Problem 判断一维线段没有两辆相交的最大线段数量
    Tex中的引号
    DAY 96 flask05
    DAY 95 flask04
    DAY 94 flask03
    DAY 93 flask02
    DAY 92 flask01
    DAY 91 爬虫05
    DAY 90 爬虫04
  • 原文地址:https://www.cnblogs.com/zhuang229/p/12237269.html
Copyright © 2011-2022 走看看