天天看點

記一次boot+dubbo+mokito 單元測試經曆

背景

項目使用的是boot+dubbo+mybatis的架構。之是以要研究單元測試,并不是因為要自動化測試、提高代碼品質、測試覆寫率等高大上的緣由。而是因為環境上的限制,我無法使用熱部署(idea社群版,用的人太少,沒法子,自己能力不足研究不了),希望通過單元測試的方式來測試自己寫的代碼。這就要求一個單元測試類的啟動最好能在3秒以内。

另外吐槽一下很多寫單元測試的部落格,絲毫也沒有提到實際執行測試需要的時間(不提時間的單元測試都是耍流氓!)。

知識點

mokito

這個單元測試,簡單來說就是要mock掉方法中所有依賴别人的地方,相當于僅僅測試代碼邏輯(或者說空架子?可能不大貼切)。舉個例子,我的項目中大部分邏輯都寫在service裡面,如下:

public AuthVO getAuthById(int id) {
        //是以需要外部支援的部分都不是我本方法需要考慮的内容,比方這個mapper的查詢
        AdminAuthority adminAuthority = authorityMapper.selectByPrimaryKey(id);
        if (Objects.isNull(adminAuthority)) {
            throw new VerifyException("不存在此權限");
        }
        // TODO 接下來要做
        return BeanUtils.copy(adminAuthority, AuthVO::new);
    }
           

實際代碼邏輯應該比較多,舉例子嘛,偷偷懶。

mock的原則是:測試這個方法,就僅僅針對這個方法,所有需要依賴别人的地方都不是此方法需要測試的内容。比方這個mapper的查詢selectByPrimaryKey,就需要mock出一個AdminAuthority 對象來替代真實的查詢結果。以便這個方法可以不依賴mapper,繼續測試。

實際寫代碼時,對于這個mapper的查詢,我們也是需要測試的,如果把這個mock掉,那麼我們需要另外的入口去測試(費勁)。同時mock的越多,意味着你需要模拟的越多,花的精力也就越多,當然你程式思考得多,代碼品質自然好。不過并不适合開發進度要求快的。特别現在前後端分離,前端等後端,測試等開發。有空的時候寫寫,還是不錯的!

mokito使用

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class AdminServiceImplMockTest {

    @Autowired
    private AdminService adminService;

    @MockBean
    private AdminMapper adminMapper;
    
    @BeforeEach
    void setUp() {
    }

    @AfterEach
    void tearDown() {
    }

    @Test
    void login() {
        Admin admin = new Admin(null,"java1234","123456");
        Mockito.when(adminMapper.login(admin)).thenReturn(new Admin(1,"java1234","123456"));
        Admin login = adminService.login(admin);
        Assertions.assertEquals(login.getId(),1);
        Assertions.assertEquals(login.getUsername(),"java1234");
        Assertions.assertEquals(login.getPassword(),"123456");
        //Mockito的單元測試其實是要啟動上下文的
    }
 }
           

具體使用參照:

https://mp.weixin.qq.com/s/mOW3Z_Qrq_LuoRWICHeHjQ

簡單來說@MockBean 聲明了這個bean由mokito去注入而不是spring,debug時可以看到這個adminMapper有“mock”字樣。

記一次boot+dubbo+mokito 單元測試經曆

對于這個測試類,@Autowired private AdminService adminService 是我們需要測試的service,@MockBean private AdminMapper adminMapper 是service裡面需要用的mapper。我們不測這個mapper,是以需要mock掉(不依賴資料查詢,就不會出現因為資料庫原因導緻單元測試一段時間後失敗的現象)。

那麼它是如何是實作的呢?

Mockito.when(adminMapper.login(admin)).thenReturn(new 
Admin(1,"java1234","123456"));
           

這句話的意思是:當調用adminMapper.login(admin)的時候,傳回一個我們mock出來的對象。這行代碼要在調用adminService.login(admin)之前執行。這樣Mockito才能在實際的adminServiceImpl執行到adminMapper.login(admin)的時候,傳回我們mock出來的對象(相當于切進去,替換掉)

記一次boot+dubbo+mokito 單元測試經曆

注:打斷點可以看到它進入了adminServiceImpl,并且adminMapper已經被Mockito成功替換,這樣傳回的對象就是我們mock出來的對象。

mokito注意點

需要注意的是mokito 啟動時需要spring的上下文的,并且service隻能通過spring進行注入不能自己new

這樣寫的話,但debug進入AdminServiceImpl裡面時,會發現adminMapper是null,而不是Mockito生成的對象。我猜想大概是因為mock出來的adminMapper是由spring容器管理的,adminService 也必須由容器管理,這樣才能在進入AdminServiceImpl時adminMapper有值。

需要上下文是指,對于junit4 必須這樣寫:

記一次boot+dubbo+mokito 單元測試經曆

因為測的service,不需要web環境是以:

webEnvironment = SpringBootTest.WebEnvironment.NONE
           

思考點:按理說,mockito 測試就是為了專注方法自己,所有外部依賴一律都mock掉。那麼實在沒有必要加載上下文,加載上下文就相當于啟動了整個項目,什麼資料庫連接配接,redis連接配接,dubbo連接配接,一堆東西。十幾二十秒就得花在這上面了。

優化單元測試啟動速度

為單元測試建立自己的啟動類

自己的啟動類可以設定懶加載,可以注釋@EnableDubbo等。總之就是一切單元測試不需要的,真實環境需要的,都可以在這個啟動類中去掉。

我建立了兩個啟動類,一個是mock的(不涉及資料庫查詢),一個是test(涉及資料庫查詢)。

記一次boot+dubbo+mokito 單元測試經曆

注:本意是希望兩個test的啟動類各子完成自己的事情。但實際測試時發現,一個test啟動類上的改動導緻啟動報錯之後,另一個test啟動類也同時報錯了。。。哎,又是原理。不過,怎麼說,至少也能有一個啟動類能用

為單元測試建立自己的yml配置檔案

記一次boot+dubbo+mokito 單元測試經曆
記一次boot+dubbo+mokito 單元測試經曆

注:直接在test下把配置檔案copy過來,test啟動類就能識别到并執行test下的配置檔案。需要注意的是 active: ‘@[email protected]’ 是多配置檔案配置,在pom中指定的,在test的yml中不能識别到。是以會報一下類似jdbc url 未定義,但實際有的問題。是以這裡直接初始化為active: ‘test’

不同的單元測試類對應自己的配置檔案

記一次boot+dubbo+mokito 單元測試經曆

在測試類加注解@ActiveProfiles(“mock”),即可指定配置檔案。奇怪的是,在啟動類中加這個注解,雖然不報錯,但沒有效果。是以隻能加在這個地方(BootMockBase作為測試類的父類)

懶加載

boot2.1.0及以下版本通過下面方式實作:

@SpringBootApplication

public class ProviderTestApp {

    public static void main(String[] args) {
        SpringApplication.run(ProviderTestApp.class, args);
    }

    //很尴尬的是service是dubbo管理的,mapper是mybatis管理的,下面的配置對它們無效,是以并沒有節約多少時間
    @Configuration
    @ComponentScan(lazyInit = true)
    static class LocalConfig{

    }


}
           

boot2.2及以上yml中有配置,網上一堆,就不寫了。

注:很尴尬的是service是dubbo管理的,mapper是mybatis管理的,下面的配置對它們無效。是以大部分類都沒有能懶加載,是以并沒有節約多少時間。

排除dubbo 失敗

dubbo會在加載上下文時,輸出一堆dubbo連接配接資訊,暴露服務資訊,總之一堆消耗了不少時間(大概在8秒内)。

本來的思路是想在單元測試時,将注入的dubbo的service轉換成spring的service,省略掉dubbo消耗的時間,畢竟我隻測service不需要dubbo。但是能力不足,沒辦法,但是找到了切入點:

@Configuration
public class BeanPostPrcessorImpl implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if(beanName.equals("adminAuthServiceImpl")){
            System.out.println("AAAA");
        }
        System.out.println("對象" + beanName + "開始執行個體化");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("對象" + beanName + "執行個體化完成");
        return bean;
    }
}
           

思路就是在對象執行個體化之前的修改為spring的service

我手工替換掉dubbo的service,然後排除dubbo的内容:

記一次boot+dubbo+mokito 單元測試經曆

在注釋掉yml中dubbo的配置後,直接報缺失dubbo application配置的資訊:

記一次boot+dubbo+mokito 單元測試經曆

總結:另外在我排除yml加密的jasypt時發現,即使項目中沒有任何地方使用到jasypt。隻要pom檔案有,端口就會有jasypt的日志資訊。這樣算不算設計上不合理?我隻是引入了這個jar,都沒有顯式的告訴它我要不要用,它就自動的為我做了一些事情。包括redis 和dubbo,我都沒有辦法排除掉它們。隻能說對boot的原理完全不懂,有時候真的很無力。

我嘗試過排除相關的bean,如下:

//正規表達式排除
@ComponentScan(
        excludeFilters = {
                @ComponentScan.Filter(
                        type = FilterType.REGEX,
                        pattern = "com.ulisesbocchio.jasyptspringboot\\..*"
                )
        }
)
//類排除
@ComponentScan(
        excludeFilters = {
                @ComponentScan.Filter(
                        type = FilterType.ASSIGNABLE_TYPE,
                        classes = {RedisConfig.class}
                )
        }
)
           

結果就是排除貌似失敗了,至于@ComponentScan排除失敗的問題,網上也有說,不過又是涉及原理的東西。。。我又想到,是否可以在單元測試時,排除引入jar包?網上沒查到相關内容,隻好作罷。

延遲暴露dubbo服務

無意中看到管理延遲暴露dubbo服務的文章,說的是先完成bean的初始化之後延遲暴露dubbo服務,這樣應該可以讓單元測試先啟動起來。

dubbo:
  application:
    #注冊在注冊中心的名稱,唯一辨別,請勿重複
    name: demo-provider
    #logger: logback
  #單zookeeper服務:zookeeper://127.0.0.1:2181
  registry:
    address: zookeeper://127.0.0.1:2181
    port: 2181 #提供注冊的端口
  provider:
    filter: dubboproviderlogfilter
    delay: 500000
           

注: delay: 500000 時間設定長一些

這樣設定之後,啟動時間節約了8秒。

其他

可以說本次,想要完成的部分都沒有完成,都被原理給卡住了。看來刻不容緩了。

繼續閱讀