單元測試中簡單使用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結果


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM