springboot集成測試容器重啟問題的處理
背景
spring boot test的項目中常用的測試框架, 最近在寫集成測試的時候發(fā)現(xiàn)一個比較奇怪的問題,當(dāng)我在運行多個測試用例的時候會偶爾重新啟動整個容器上下文,由于后期業(yè)務(wù)逐漸復(fù)雜,大量的測試用例需要運行,這個問題直接導(dǎo)致回歸測試的效率降低。

舉個例子:

幾個類:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class)
public class BaseApiTest {
@Test
public void init() {
}
}
public class ApiTest1 extends BaseApiTest {
@MockBean
private Service service;
@Test
public void test1() {
service.call();
}
}
public class ApiTest2 extends BaseApiTest {
@Autowired
private Service service;
@Test
public void test2() {
service.call();
}
}
@SpringBootApplication
@Slf4j
public class TestApplication {
public static void main(String[] args) {
log.info("啟動容器");
new SpringApplication(TestApplication.class).run(args);
}
}
@Component
public class Service {
public void call() {
System.out.println("service called");
}
}
運行test包下所有測試:

發(fā)現(xiàn)容器重復(fù)啟動了。
測試用例的運行流程
可以開啟idea的線程堆棧跟蹤,觀察整個容器的啟動路徑

com.intellij.rt.junit是idea內(nèi)部的實現(xiàn),點擊idea的運行單測會觸發(fā)JunitStarter的main函數(shù)去啟動,可以去GitHub找到源碼:

做一些準備工作找到指定的runner就開始調(diào)用junit的包去執(zhí)行編寫的單測,junit為了靈活的擴展不同的測試運行環(huán)境,類似SPI機制動態(tài)獲取Runner去運行單測。例如我的例子里指定了SpringRunner就是需要依賴Spring容器的一個實現(xiàn),這樣就讓測試用例可以運行在Spring環(huán)境中。

junit的入口也支持在測例前后去插入一些操作,自己去實現(xiàn)RunnerListener即可。junit默認實現(xiàn)了監(jiān)聽器去記錄測例的耗時,失敗的數(shù)量等信息。

我指定的Runner是SpringRunner,其與SpringJUnit4ClassRunner并沒什么區(qū)別,可以看其實現(xiàn)完全繼承了SpringJUnit4ClassRunner的實現(xiàn)。
所以我們直接看SpringJUnit4ClassRunner的runner實現(xiàn):

它首先判斷了當(dāng)前的環(huán)境是否需要忽略單測。如果忽略會在通知里得到通知。關(guān)于環(huán)境的指定控制可以參考注解@IfProfileValue,判斷環(huán)境正確之后繼續(xù)調(diào)用junit包父類ParentRunner的方法執(zhí)行。
其定義了執(zhí)行的基本的模板:

classBlock里面定義線程池執(zhí)行和測例執(zhí)行的一些before和after邏輯,里面的runChild是抽象方法,也是留給各個Runner實現(xiàn)的鉤子。getFilteredChildren能夠根據(jù)@Test注解拿到所有需要運行的用例方法,然后每個方法去調(diào)用具體的Runner運行。

其流程圖如下


SpringJUnit4ClassRunner的運行每個方法會給每個測例方法進行一個封裝成Statement。關(guān)鍵就在methodBlock方法,它實現(xiàn)了Spring boot對方法運行的封裝

createTest會在測試的上下文里維護一個配置,然后會用通知機制一樣去依次調(diào)用需要準備的東西,其中就包含spring容器的上下文。


其會執(zhí)行injectDependencies,處理依賴的bean準備。TestContext是每個單測方法需要運行的上下文,在Spring boot的測試環(huán)境下,其維護了Spring的上下文,每個方法的執(zhí)行都會去獲取Spring的上下文

根據(jù)單測的相關(guān)信息文獲取上spring的上下文,為了避免每次都去加載容器,TestContext會維護一個spring容器的緩存,CacheAwareContextLoaderDelegate


CacheAwareContextLoaderDelegate其內(nèi)部的獲取又是通過單測配置信息去ContextCache獲取的,其內(nèi)部是一個同步SynchronizedMap去保存的。

其內(nèi)部實現(xiàn)看測例的配置信息去獲取加載過的容器,如果沒獲取到就會觸發(fā)重新加載新容器的流程,所以關(guān)鍵就是看key在Map中的獲取邏輯,其底層是spring test自己實現(xiàn)一個的Map

可以看到其是基于HashMap的一個哈希結(jié)構(gòu),根據(jù)Jdk的源碼,我們可以知道HashMap的key是根據(jù)hashCode與equals去比較key,那可以確定,要想復(fù)用同一個容器就得看Key值的hashCode和equals實現(xiàn)。接下來我們看MergedContextConfiguration源碼.


可以發(fā)現(xiàn)其比較的值是:
* @param testClass the test class for which the configuration was merged
* @param locations the merged context resource locations
* @param classes the merged annotated classes
* @param contextInitializerClasses the merged context initializer classes
* @param activeProfiles the merged active bean definition profiles
* @param propertySourceLocations the merged {@code PropertySource} locations
* @param propertySourceProperties the merged {@code PropertySource} properties
* @param contextCustomizers the context customizers
* @param contextLoader the resolved {@code ContextLoader}
* @param cacheAwareContextLoaderDelegate a cache-aware context loader
* delegate with which to retrieve the parent context
* @param parent the parent configuration or {@code null} if there is no parent
這些參數(shù)確定了能否共享SpringApplication,那兩個測試類一個@Autowired,另一個使用@MockBean,肯定是改變這里面某個值,我們可以回溯這個MergedContextConfiguration是在什么時候被初始化的。這個還要追溯到idea的啟動類,找到Runner的時候,SpringJUnit4ClassRunner的初始化的過程。
在每個測試類的運行都會喚起SpringJUnit4ClassRunner初始化,調(diào)用構(gòu)造函數(shù)的時候會去加載測試類的上下文

去創(chuàng)建這個TextContextManager

這里首先會根據(jù)被測試類的繼承關(guān)系和注解的遞歸去找到固定包下面被注解@BootstrapWith修飾的類,因為是Spring boot test這里會根據(jù)@SpringBootTest 注解找到SpringBootTestContextBootstrapper類,找到這個引導(dǎo)類之后就會去初始化MergedContextConfiguration了。

引導(dǎo)類通過SPI機制加載到所有的Customizer,并根據(jù)需要DefinitionsParser,進行轉(zhuǎn)換,保存在MergedContextConfiguration的一個字段,mock的一個屬性會在轉(zhuǎn)換的時候記錄到,而非mock的contextCustomizers則不會記錄。
注意這里提到的


兩個類一個用mock的字段,一個用非mock的字段,兩個MockitoContextCustomizer的definitions就不一樣,因此無法共享上下文,因此需要重新啟動一個Spring容器,并存放到CacheAwareContextLoaderDelegate,以便后面共享。
結(jié)論
分析源碼的設(shè)計,發(fā)現(xiàn)應(yīng)用了很多SPI與可擴展的設(shè)計,idea與junit的解耦,junit的抽象與模板定義與各個測試框架的擴展。
針對容器重啟的角度,對于一個類來說,一定是共享一個spring上下文,但是不同的類可能由于注入的bean的方式不同導(dǎo)致無法共享spring上下文,所以導(dǎo)致重啟會浪費掉一些時間,因此建議確定好mock的邊界,對盡量多的測例共享一個容器視角可以提高單測效率,基于此可以設(shè)計多繼承關(guān)系的單測結(jié)構(gòu),并把注入的bean向上共享,避免各個測試子類自己去注入出現(xiàn)不一致的情況。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
在Eclipse IDE使用Gradle構(gòu)建應(yīng)用程序(圖文)
這篇文章主要介紹了在Eclipse IDE使用Gradle構(gòu)建應(yīng)用程序(圖文),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-12-12
Mybatis 動態(tài)表名+Map參數(shù)傳遞+批量操作詳解
這篇文章主要介紹了Mybatis 動態(tài)表名+Map參數(shù)傳遞+批量操作詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
java開發(fā)Dubbo注解Adaptive實現(xiàn)原理
這篇文章主要為大家介紹了java開發(fā)Dubbo注解Adaptive實現(xiàn)原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09
springboot aop切到service層,不生效問題
這篇文章主要介紹了springboot aop切到service層,不生效問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05
java數(shù)據(jù)結(jié)構(gòu)算法稀疏數(shù)組示例詳解
這篇文章主要為大家介紹了java數(shù)據(jù)結(jié)構(gòu)算法稀疏數(shù)組示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06

