測試用例千萬不能隨便,記錄由一個測試用例異常引起的思考

2022-07-26 12:00:33

測試用例大家平時寫不寫?

我以前寫測試用例只是針對業務介面,每個介面寫一個,資料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個概念,這裡只是表示所有的測試用例執行完,跑了所有程式碼行的比例。

最後希望大家千萬不能忽視測試用例,雖然有時我寫的想吐,但是最後你會體會到它的甜。