我以前寫測試用例只是針對業務接口,每個接口寫一個,數據case也只是測一種。能跑通就可以了。要不同的場景case,那就改數據。重新跑一遍。簡單省事。
但是自從我業余時間開始維護開源后,開始加深了對測試用例的理解。甚至我現在已經把測試用例的地位提升了與核心代碼一樣重要的地位,我曾戲稱過光寫核心代碼不寫測試用例代碼的都是耍流氓行為。
開源項目面對的是的所有人,每個人每個公司的環境都不同,項目結構也不一樣,jdk,spring體系的版本,第三方依賴包都不一樣。所以開源框架必須要在所有的場景下都工作正常。這么多功能點,這么多場景,哪怕我是作者,光靠熟悉度是不可能記起來那么多細節點的,這時候測試用例就顯得非常重要了,它是整個項目的最關鍵的質量保障。很多時候,我都是靠測試用例來發現一些邊緣細小的bug的。目前我的開源項目擁有870個測試用例,覆蓋了大概90%以上的場景。
這篇文章探討一個由測試用例引發的測試用例運行機制的問題。
事情的起因是一個群里的小伙伴發現某一個單元測試用例在配置項錯誤的時候,spring上下文竟然執行了2次,而在正確配置的情況下,是正常只啟動了一次。這讓他很不解,以為是框架出了問題。
他之所以覺得spring啟動了2次,是看到日志中出現了2次springboot的logo打印,2次一模一樣的報錯:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.5.RELEASE)
com.yomahub.liteflow.exception.ELParseException: 程序錯誤,不滿足語法規范,沒有匹配到合適的語法,最大匹配致[0:7]
at com.yomahub.liteflow.builder.el.LiteFlowChainELBuilder.setEL(LiteFlowChainELBuilder.java:124) ~[liteflow-core-2.8.2.jar:na]
at com.yomahub.liteflow.parser.helper.ParserHelper.parseOneChainEl(ParserHelper.java:391) ~[liteflow-core-2.8.2.jar:na]
at com.yomahub.liteflow.parser.el.XmlFlowELParser.parseOneChain(XmlFlowELParser.java:20) ~[liteflow-core-2.8.2.jar:na]
at java.util.ArrayList.forEach(ArrayList.java:1259) ~[na:1.8.0_292]
at com.yomahub.liteflow.parser.helper.ParserHelper.parseDocument(ParserHelper.java:217) ~[liteflow-core-2.8.2.jar:na]
at com.yomahub.liteflow.parser.base.BaseXmlFlowParser.parse(BaseXmlFlowParser.java:40) ~[liteflow-core-2.8.2.jar:na]
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.5.RELEASE)
com.yomahub.liteflow.exception.ELParseException: 程序錯誤,不滿足語法規范,沒有匹配到合適的語法,最大匹配致[0:7]
at com.yomahub.liteflow.builder.el.LiteFlowChainELBuilder.setEL(LiteFlowChainELBuilder.java:124) ~[liteflow-core-2.8.2.jar:na]
at com.yomahub.liteflow.parser.helper.ParserHelper.parseOneChainEl(ParserHelper.java:391) ~[liteflow-core-2.8.2.jar:na]
at com.yomahub.liteflow.parser.el.XmlFlowELParser.parseOneChain(XmlFlowELParser.java:20) ~[liteflow-core-2.8.2.jar:na]
at java.util.ArrayList.forEach(ArrayList.java:1259) ~[na:1.8.0_292]
at com.yomahub.liteflow.parser.helper.ParserHelper.parseDocument(ParserHelper.java:217) ~[liteflow-core-2.8.2.jar:na]
at com.yomahub.liteflow.parser.base.BaseXmlFlowParser.parse(BaseXmlFlowParser.java:40) ~[liteflow-core-2.8.2.jar:na]
測試用例代碼為:
@RunWith(SpringRunner.class)
@TestPropertySource(value = "classpath:/whenTimeOut/application1.properties")
@SpringBootTest(classes = WhenTimeOutELSpringbootTestCase.class)
@EnableAutoConfiguration
@ComponentScan({"com.yomahub.liteflow.test.whenTimeOut.cmp"})
public class WhenTimeOutELSpringbootTestCase {
@Resource
private FlowExecutor flowExecutor;
//其中b和c在when情況下超時,所以拋出了WhenTimeoutException這個錯
@Test
public void testWhenTimeOut1() throws Exception{
LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
Assert.assertFalse(response.isSuccess());
Assert.assertEquals(WhenTimeoutException.class, response.getCause().getClass());
}
}
開源框架在源代碼層面,不可能主動去再次啟動spring上下文(事實上想做我也不知道如何去做)。而且正確配置情況下,是正常的。而且spring的@Configuration的也啟動了2次,從線程堆棧上來看,也是由Junit這里觸發的:
值得一提的是,報出的錯是在springboot啟動環節。所以壓根就沒進入@Test修飾的測試用例代碼里。所以和代碼寫什么沒有關系。我測試了下,如果在測試代碼里拋出異常,spring上下文是只啟動一次的。
所以這個問題可能到這就結束了,因為并非框架本身的問題,Junit本身在啟動spring失敗的情況觸發了2次初始化spring的動作,可能是一種Junit的重試的機制。這并非我能控制,反正真的有錯,也會拋出來,也不用care具體初始化幾次,也不影響我的測試用例的整體效果的,把具體測試用例改對就行了。
但是我之后在處理一個測試用例時突然想到了關于測試用例的Spring加載的機制,從而聯想到之前的問題。突然恍然大悟。
我們用例的結構一般都是,一個測試用例代表了一個大的場景,里面的每一個方法代表了一種具體的case。假設1個類帶上10個test具體用例,那么當你點擊類上的Run Test的時候,spring會被初始化多少次呢。
答案是1次,springboot test為了加快運行測試用例的過程,不可能每一個方法都去初始化一遍spring的。在這一個類里的spring的上下文都會緩存起來,這10個方法都會共享同一個spring上下文。
具體的運行機制是:在點下類的Run Test的時候,會去先初始化spring,然后開始運行一個個測試方法,當測試方法運行的時候,如果發現沒有初始化spring,還會初始化一遍spring。這就解釋了,當我們單獨運行方法的run test的時候,也會初始化一遍spring。
現在就可以解釋前文的問題了,因為初始化失敗了,在運行方法時發現還沒初始化,所以又進行了初始化。
但是對于不同的Test類的話,還是會初始化多遍的。也就是說,每一個類都會初始化一遍spring。這在你運行多個測試用例時應該能發現。
再額外引申一個問題:有沒有人碰到過運行所有測試用例時總會有幾個一直報錯,但是單個運行卻又完全正常的問題呢?
如果你有碰到過的話,那一定是忽略了以下這個注意點:
如果你選擇全部運行測試用例,雖然每個測試用例類初始化一遍spring,但是JVM從始至終卻只啟動了一次。而你那些定義在類里的static的變量,不會隨著spring啟動而發生變化。當你全部運行的時候,有可能你出錯的測試用例某些引用的static變量還是上個測試用例遺留下來的數據。所以可能會報錯。而單次運行的時候,則沒有這種現象。
如果你碰到了這種情況,你得在測試用例里使用@AfterClass這個注解,在注解聲明的方法里把這次測試用例中的static變量給清空。這樣就可以一起去運行了。例如我的每一個測試用例都去去繼承一個BaseTest方法,在里面寫上這個方法用于清空static的緩存:
public class BaseTest {
@AfterClass
public static void cleanScanCache(){
ComponentScanner.cleanCache();
FlowBus.cleanCache();
ExecutorHelper.loadInstance().clearExecutorServiceMap();
SpiFactoryCleaner.clean();
LiteflowConfigGetter.clean();
}
}
關于測試用例該怎么寫,有什么常用的寫法。這里不作過多說明,自己百度一下,應該可以找到一大把教程,或者有興趣,也可以去閱讀我的開源項目LiteFlow中的測試用例。
測試用例除了可以確保你的項目質量,還可以清晰的看到你整個測試用例覆蓋了你多少的代碼行。我這里的測試用例是單獨列工程去寫的。用以區別核心工程包。
然后在IDEA里去單獨配置執行testcase的任務:
然后去點run xxx with coverage按鈕運行測試用例:
多個測試工程之間,運行好一個會彈出對話框問你是否想把這次的結果加入到總的結果里去,直接點add就可以了:
你所有的測試用例工程運行好,在右側會得出一個如下的報告頁面:
這里在最上面可以看到我整個測試用例的覆蓋行數是79%。但這并不表示項目覆蓋場景只有79%。行覆蓋和功能場景覆蓋是2個概念,這里只是表示所有的測試用例運行完,跑了所有代碼行的比例。
最后希望大家千萬不能忽視測試用例,雖然有時我寫的想吐,但是最后你會體會到它的甜。
本文內容不用于商業目的,如涉及知識產權問題,請權利人聯系51Testing小編(021-64471599-8017),我們將立即處理