前言

最近在做項目里的自動化測試工作,使用的是TestNG測試框架,主要涉及的測試類型有接口測試以及基于業(yè)務實際場景的場景化測試。由于涉及的場景大多都是大數(shù)據(jù)的作業(yè)開發(fā)及執(zhí)行(如MapReduce、Spark、Hql等任務的執(zhí)行),而這些任務的執(zhí)行都需要耗費較多的時間。舉一個普遍的例子,其中一條場景測試用例是:

  • 執(zhí)行一個MapReduce作業(yè),校驗作業(yè)的執(zhí)行結果和執(zhí)行日志。

對于一個最簡單的MR任務,如果YARN集群資源充足,它的執(zhí)行時間也要花上將近一分鐘的時間。更不用說當YARN集群計算資源飽和時,任務還需要持續(xù)等待資源分配等。當測試回歸用例集里包含了大量此類的用例時,如果還用傳統(tǒng)的單線程執(zhí)行方式,則一次自動化回歸將會耗費大量的時間。

多線程并行執(zhí)行

基于上述場景,我們可以考慮將自動化用例中相互之間沒有耦合關系,相對獨立的用例進行并行執(zhí)行。如,我可以通過起不同的線程同時去執(zhí)行不同的MR任務、Spark任務,每個線程各自負責跟蹤任務的執(zhí)行情況。

此外,即使是單純的接口自動化測試,如果測試集里包含了大量的用例時,我們也可以借助于TestNG的多線程方式提高執(zhí)行速度。

必須要指出的是,通過多線程執(zhí)行用例時雖然可以大大提升用例的執(zhí)行效率,但是我們在設計用例時也要考慮到這些用例是否適合并發(fā)執(zhí)行,以及要注意多線程方式的通病:線程安全與共享變量的問題。建議是在測試代碼中,盡可能地避免使用共享變量。如果真的用到了,要慎用synchronized關鍵字來對共享變量進行加鎖同步。否則,難免你的用例執(zhí)行時可能會出現(xiàn)不穩(wěn)定的情景(經(jīng)常聽到有人提到用例執(zhí)行地不穩(wěn)定,有時100%通過,有時只有90%通過,猜測可能有一部分原因也是這個導致的)。

TestNG中的多線程使用姿勢

不同級別的并發(fā)

通常,在TestNG的執(zhí)行中,測試的級別由上至下可以分為suite -> test -> class -> method,箭頭的左邊元素跟右邊元素的關系是一對多的包含關系。

這里的test指的是testng.xml中的test tag,而不是測試類里的一個 @Test。測試類里的一個 @Test實際上對應這里的method。所以我們在使用 @BeforeSuite、 @BeforeTest、 @BeforeClass、 @BeforeMethod這些標簽的時候,它們的實際執(zhí)行順序也是按照這個級別來的。

suite

一般情況下,一個testng.xml只包含一個suite。如果想起多個線程執(zhí)行不同的suite,官方給出的方法是:通過命令行的方式來指定線程池的容量。

java org.testng.TestNG -suitethreadpoolsize 3 testng1.xml testng2.xml testng3.xml

即可通過三個線程來分別執(zhí)行testng1.xml、testng2.xml、testng3.xml。
實際上這種情況在實際中應用地并不多見,我們的測試用例往往放在一個suite中,如果真需要執(zhí)行不同的suite,往往也是在不同的環(huán)境中去執(zhí)行,屆時也自然而然會做一些其他的配置(如環(huán)境變量)更改,會有不同的進程去執(zhí)行。因此這種方式不多贅述。

test, class, method

test,class,method級別的并發(fā),可以通過在testng.xml中的suite tag下設置,如:

<suite name="Testng Parallel Test" parallel="tests" thread-count="5">
<suite name="Testng Parallel Test" parallel="classes" thread-count="5">
<suite name="Testng Parallel Test" parallel="methods" thread-count="5">

它們的共同點都是最多起5個線程去同時執(zhí)行不同的用例。
它們的區(qū)別如下:

  • tests級別:不同test tag下的用例可以在不同的線程執(zhí)行,相同test tag下的用例只能在同一個線程中執(zhí)行。

  • classs級別:不同class tag下的用例可以在不同的線程執(zhí)行,相同class tag下的用例只能在同一個線程中執(zhí)行。

  • methods級別:所有用例都可以在不同的線程去執(zhí)行。

搞清楚并發(fā)的級別非常重要,可以幫我們合理地組織用例,比如將非線程安全的測試類或group統(tǒng)一放到一個test中,這樣在并發(fā)的同時又可以保證這些類里的用例是單線程執(zhí)行。也可以根據(jù)需要設定class級別的并發(fā),讓同一個測試類里的用例在同一個線程中執(zhí)行。

并發(fā)時的依賴

實踐中,很多時候我們在測試類中通過dependOnMethods/dependOnGroups方式,給很多測試方法的執(zhí)行添加了依賴,以達到期望的執(zhí)行順序。如果同時在運行testng時配置了methods級別并發(fā)執(zhí)行,那么這些測試方法在不同線程中執(zhí)行,還會遵循依賴的執(zhí)行順序嗎?答案是——YES。牛逼的TestNG就是能在多線程情況下依然遵循既定的用例執(zhí)行順序去執(zhí)行。

不同dataprovider的并發(fā)

在使用TestNG做自動化測試時,基本上大家都會使用dataprovider來管理一個用例的不同測試數(shù)據(jù)。而上述在testng.xml中修改suite標簽的方法,并不適用于dataprovider多組測試數(shù)據(jù)之間的并發(fā)。執(zhí)行時會發(fā)現(xiàn),一個dp中的多組數(shù)據(jù)依然是順序執(zhí)行。

解決方式是:在 @DataProvider中添加parallel=true。
如:

import org.testng.annotations.DataProvider;import testdata.ScenarioTestData;public class ScenarioDataProvider {    @DataProvider(name = "hadoopTest", parallel=true)    public static Object [][] hadoopTest(){        return new Object[][]{
            ScenarioTestData.hadoopMain,
            ScenarioTestData.hadoopRun,
            ScenarioTestData.hadoopDeliverProps
        };
    }    
    @DataProvider(name = "sparkTest", parallel=true)    public static Object [][] sparkTest(){        return new Object[][]{
            ScenarioTestData.spark_java_version_default,
            ScenarioTestData.spark_java_version_162,
            ScenarioTestData.spark_java_version_200,
            ScenarioTestData.spark_python
        };
    }    
    @DataProvider(name = "sqoopTest", parallel=true)    public static Object [][] sqoopTest(){        return new Object[][]{
            ScenarioTestData.sqoop_mysql2hive,
            ScenarioTestData.sqoop_mysql2hdfs
        };
    }
}

默認情況下,dp并行執(zhí)行的線程池容量為10,如果要更改并發(fā)的數(shù)量,也可以在suite tag下指定參數(shù)data-provider-thread-count:

<suite name="Testng Parallel Test" parallel="methods" thread-count="5" data-provider-thread-count="20" >

同一個方法的并發(fā)

有些時候,我們需要對一個測試用例,比如一個http接口,執(zhí)行并發(fā)測試,即一個接口的反復調(diào)用。TestNG中也提供了優(yōu)雅的支持方式,在 @Test標簽中指定threadPoolSize和invocationCount。

@Test(enabled=true, dataProvider="testdp", threadPoolSize=5, invocationCount=10)

其中threadPoolSize表明用于調(diào)用該方法的線程池容量,該例就是同時起5個線程并行執(zhí)行該方法;invocationCount表示該方法總計需要被執(zhí)行的次數(shù)。該例子中5個線程同時執(zhí)行,當總計執(zhí)行次數(shù)達到10次時,停止。

注意,該線程池與dp的并發(fā)線程池是兩個獨立的線程池。這里的線程池是用于起多個method,而每個method的測試數(shù)據(jù)由dp提供,如果這邊dp里有3組數(shù)據(jù),那么實際上10次執(zhí)行,每次都會調(diào)3次接口,這個接口被調(diào)用的總次數(shù)是10*3=30次。threadPoolSize指定的5個線程中,每個線程單獨去調(diào)method時,用到的dp如果也是支持并發(fā)執(zhí)行的話,會創(chuàng)建一個新的線程池(dpThreadPool)來并發(fā)執(zhí)行測試數(shù)據(jù)。

示例代碼如下:

package testng.parallel.test;import java.text.SimpleDateFormat;import java.util.Date;import org.testng.annotations.AfterClass;import org.testng.annotations.BeforeClass;import org.testng.annotations.DataProvider;import org.testng.annotations.Test;public class TestClass1 {    private SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//設置日期格式
    @BeforeClass
    public void beforeClass(){
        System.out.println("Start Time: " + df.format(new Date()));
    }        
    @Test(enabled=true, dataProvider="testdp", threadPoolSize=2, invocationCount=5)    public void test(String dpNumber) throws InterruptedException{
        System.out.println("Current Thread Id: " + Thread.currentThread().getId() + ". Dataprovider number: "+ dpNumber);
        Thread.sleep(5000);
    }    @DataProvider(name = "testdp", parallel = true)    public static Object[][]testdp(){        return new Object[][]{
            {"1"},
            {"2"}
        };
    }    
    @AfterClass
    public void afterClass(){
        System.out.println("End Time: " + df.format(new Date()));
    }
}

測試結果:

Start Time: 2017-03-11 14:10:43[ThreadUtil] Starting executor timeOut:0ms workers:5 threadPoolSize:2Current Thread Id: 14. Dataprovider number: 2Current Thread Id: 15. Dataprovider number: 2Current Thread Id: 12. Dataprovider number: 1Current Thread Id: 13. Dataprovider number: 1Current Thread Id: 16. Dataprovider number: 1Current Thread Id: 18. Dataprovider number: 1Current Thread Id: 17. Dataprovider number: 2Current Thread Id: 19. Dataprovider number: 2Current Thread Id: 21. Dataprovider number: 2Current Thread Id: 20. Dataprovider number: 1End Time: 2017-03-11 14:10:58

Other TestNG Tips

TestNG作為一個成熟的、業(yè)界廣泛使用的測試框架,自然有其存在的合理性。這邊再分享一些簡單有用的標簽,具體的使用姿勢大家可以自己去探索,官網(wǎng)有比較全的介紹,畢竟自己探索的才會印象深刻。

  1. groups/dependsOnGroups/dependsOnMethods ——設置用例間依賴

  2. dataProviderClass ——將dataprovider單獨放到一個專用的類中,實現(xiàn)測試代碼、dataprovider、測試數(shù)據(jù)分層。

  3. timeout ——設置用例的超時時間(并發(fā)/非并發(fā)都可支持)

  4. alwaysRun ——某些依賴的用例失敗了,導致用例被跳過。對于一些為了保持環(huán)境干凈而“掃尾”的測試類,如果我們想強制執(zhí)行可以使用此標簽。

  5. priority ——設置優(yōu)先級,讓某些測試用例被更大概率優(yōu)先執(zhí)行。

  6. singleThreaded ——強制一個class類里的用例在一個線程執(zhí)行,忽視method級別并發(fā)

  7. preserve-order ——指定是否按照testng.xml中的既定用例順序執(zhí)行用例

總結

在TestNG中使用多線程的方式并行執(zhí)行測試用例可以有效提供用例的執(zhí)行速度,而且TestNG對多線程提供了很好的支持,即使是菜鳥也可以方便地上手多線程。此外,TestNG默認會使用線程池的方式創(chuàng)建線程,減小了程序的開銷。

參考鏈接