背景
項目使用的是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”字樣。

對于這個測試類,@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出來的對象(相當于切進去,替換掉)
注:打斷點可以看到它進入了adminServiceImpl,并且adminMapper已經被Mockito成功替換,這樣傳回的對象就是我們mock出來的對象。
mokito注意點
需要注意的是mokito 啟動時需要spring的上下文的,并且service隻能通過spring進行注入不能自己new
這樣寫的話,但debug進入AdminServiceImpl裡面時,會發現adminMapper是null,而不是Mockito生成的對象。我猜想大概是因為mock出來的adminMapper是由spring容器管理的,adminService 也必須由容器管理,這樣才能在進入AdminServiceImpl時adminMapper有值。
需要上下文是指,對于junit4 必須這樣寫:
因為測的service,不需要web環境是以:
webEnvironment = SpringBootTest.WebEnvironment.NONE
思考點:按理說,mockito 測試就是為了專注方法自己,所有外部依賴一律都mock掉。那麼實在沒有必要加載上下文,加載上下文就相當于啟動了整個項目,什麼資料庫連接配接,redis連接配接,dubbo連接配接,一堆東西。十幾二十秒就得花在這上面了。
優化單元測試啟動速度
為單元測試建立自己的啟動類
自己的啟動類可以設定懶加載,可以注釋@EnableDubbo等。總之就是一切單元測試不需要的,真實環境需要的,都可以在這個啟動類中去掉。
我建立了兩個啟動類,一個是mock的(不涉及資料庫查詢),一個是test(涉及資料庫查詢)。
注:本意是希望兩個test的啟動類各子完成自己的事情。但實際測試時發現,一個test啟動類上的改動導緻啟動報錯之後,另一個test啟動類也同時報錯了。。。哎,又是原理。不過,怎麼說,至少也能有一個啟動類能用
為單元測試建立自己的yml配置檔案
注:直接在test下把配置檔案copy過來,test啟動類就能識别到并執行test下的配置檔案。需要注意的是 active: ‘@[email protected]’ 是多配置檔案配置,在pom中指定的,在test的yml中不能識别到。是以會報一下類似jdbc url 未定義,但實際有的問題。是以這裡直接初始化為active: ‘test’
不同的單元測試類對應自己的配置檔案
在測試類加注解@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的内容:
在注釋掉yml中dubbo的配置後,直接報缺失dubbo application配置的資訊:
總結:另外在我排除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秒。
其他
可以說本次,想要完成的部分都沒有完成,都被原理給卡住了。看來刻不容緩了。