前言
近在做項(xiàng)目里的自動(dòng)化測(cè)試工作,使用的是TestNG測(cè)試框架,主要涉及的測(cè)試類型有接口測(cè)試以及基于業(yè)務(wù)實(shí)際場(chǎng)景的場(chǎng)景化測(cè)試。由于涉及的場(chǎng)景大多都是大數(shù)據(jù)的作業(yè)開(kāi)發(fā)及執(zhí)行(如MapReduce、Spark、Hql等任務(wù)的執(zhí)行),而這些任務(wù)的執(zhí)行都需要耗費(fèi)較多的時(shí)間。舉一個(gè)普遍的例子,其中一條場(chǎng)景測(cè)試用例是:
執(zhí)行一個(gè)MapReduce作業(yè),校驗(yàn)作業(yè)的執(zhí)行結(jié)果和執(zhí)行日志。
對(duì)于一個(gè)簡(jiǎn)單的MR任務(wù),如果YARN集群資源充足,它的執(zhí)行時(shí)間也要花上將近一分鐘的時(shí)間。更不用說(shuō)當(dāng)YARN集群計(jì)算資源飽和時(shí),任務(wù)還需要持續(xù)等待資源分配等。當(dāng)測(cè)試回歸用例集里包含了大量此類的用例時(shí),如果還用傳統(tǒng)的單線程執(zhí)行方式,則一次自動(dòng)化回歸將會(huì)耗費(fèi)大量的時(shí)間。
多線程并行執(zhí)行
基于上述場(chǎng)景,我們可以考慮將自動(dòng)化用例中相互之間沒(méi)有耦合關(guān)系,相對(duì)獨(dú)立的用例進(jìn)行并行執(zhí)行。如,我可以通過(guò)起不同的線程同時(shí)去執(zhí)行不同的MR任務(wù)、Spark任務(wù),每個(gè)線程各自負(fù)責(zé)跟蹤任務(wù)的執(zhí)行情況。
此外,即使是單純的接口自動(dòng)化測(cè)試,如果測(cè)試集里包含了大量的用例時(shí),我們也可以借助于TestNG的多線程方式提高執(zhí)行速度。
必須要指出的是,通過(guò)多線程執(zhí)行用例時(shí)雖然可以大大提升用例的執(zhí)行效率,但是我們?cè)谠O(shè)計(jì)用例時(shí)也要考慮到這些用例是否適合并發(fā)執(zhí)行,以及要注意多線程方式的通。壕程安全與共享變量的問(wèn)題。建議是在測(cè)試代碼中,盡可能地避免使用共享變量。如果真的用到了,要慎用synchronized關(guān)鍵字來(lái)對(duì)共享變量進(jìn)行加鎖同步。否則,難免你的用例執(zhí)行時(shí)可能會(huì)出現(xiàn)不穩(wěn)定的情景(經(jīng)常聽(tīng)到有人提到用例執(zhí)行地不穩(wěn)定,有時(shí)通過(guò),有時(shí)只有90%通過(guò),猜測(cè)可能有一部分原因也是這個(gè)導(dǎo)致的)。
TestNG中的多線程使用姿勢(shì)
不同級(jí)別的并發(fā)
通常,在TestNG的執(zhí)行中,測(cè)試的級(jí)別由上至下可以分為suite -> test -> class -> method,箭頭的左邊元素跟右邊元素的關(guān)系是一對(duì)多的包含關(guān)系。
這里的test指的是testng.xml中的test tag,而不是測(cè)試類里的一個(gè) @Test。測(cè)試類里的一個(gè) @Test實(shí)際上對(duì)應(yīng)這里的method。所以我們?cè)谑褂?@BeforeSuite、 @BeforeTest、 @BeforeClass、 @BeforeMethod這些標(biāo)簽的時(shí)候,它們的實(shí)際執(zhí)行順序也是按照這個(gè)級(jí)別來(lái)的。
suite
一般情況下,一個(gè)testng.xml只包含一個(gè)suite。如果想起多個(gè)線程執(zhí)行不同的suite,官方給出的方法是:通過(guò)命令行的方式來(lái)指定線程池的容量。
java org.testng.TestNG -suitethreadpoolsize 3 testng1.xml testng2.xml testng3.xml
即可通過(guò)三個(gè)線程來(lái)分別執(zhí)行testng1.xml、testng2.xml、testng3.xml。
實(shí)際上這種情況在實(shí)際中應(yīng)用地并不多見(jiàn),我們的測(cè)試用例往往放在一個(gè)suite中,如果真需要執(zhí)行不同的suite,往往也是在不同的環(huán)境中去執(zhí)行,屆時(shí)也自然而然會(huì)做一些其他的配置(如環(huán)境變量)更改,會(huì)有不同的進(jìn)程去執(zhí)行。因此這種方式不多贅述。
test, class, method
test,class,method級(jí)別的并發(fā),可以通過(guò)在testng.xml中的suite tag下設(shè)置,如:
<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">
它們的共同點(diǎn)都是多起5個(gè)線程去同時(shí)執(zhí)行不同的用例。
它們的區(qū)別如下:
tests級(jí)別:不同test tag下的用例可以在不同的線程執(zhí)行,相同test tag下的用例只能在同一個(gè)線程中執(zhí)行。
classs級(jí)別:不同class tag下的用例可以在不同的線程執(zhí)行,相同class tag下的用例只能在同一個(gè)線程中執(zhí)行。
methods級(jí)別:所有用例都可以在不同的線程去執(zhí)行。
搞清楚并發(fā)的級(jí)別非常重要,可以幫我們合理地組織用例,比如將非線程安全的測(cè)試類或group統(tǒng)一放到一個(gè)test中,這樣在并發(fā)的同時(shí)又可以保證這些類里的用例是單線程執(zhí)行。也可以根據(jù)需要設(shè)定class級(jí)別的并發(fā),讓同一個(gè)測(cè)試類里的用例在同一個(gè)線程中執(zhí)行。
并發(fā)時(shí)的依賴
實(shí)踐中,很多時(shí)候我們?cè)跍y(cè)試類中通過(guò)dependOnMethods/dependOnGroups方式,給很多測(cè)試方法的執(zhí)行添加了依賴,以達(dá)到期望的執(zhí)行順序。如果同時(shí)在運(yùn)行testng時(shí)配置了methods級(jí)別并發(fā)執(zhí)行,那么這些測(cè)試方法在不同線程中執(zhí)行,還會(huì)遵循依賴的執(zhí)行順序嗎?答案是——YES。牛逼的TestNG是能在多線程情況下依然遵循既定的用例執(zhí)行順序去執(zhí)行。
不同dataprovider的并發(fā)
在使用TestNG做自動(dòng)化測(cè)試時(shí),基本上大家都會(huì)使用dataprovider來(lái)管理一個(gè)用例的不同測(cè)試數(shù)據(jù)。而上述在testng.xml中修改suite標(biāo)簽的方法,并不適用于dataprovider多組測(cè)試數(shù)據(jù)之間的并發(fā)。執(zhí)行時(shí)會(huì)發(fā)現(xiàn),一個(gè)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
};
}
}
默認(rèn)情況下,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" >
同一個(gè)方法的并發(fā)
有些時(shí)候,我們需要對(duì)一個(gè)測(cè)試用例,比如一個(gè)http接口,執(zhí)行并發(fā)測(cè)試,即一個(gè)接口的反復(fù)調(diào)用。TestNG中也提供了優(yōu)雅的支持方式,在 @Test標(biāo)簽中指定threadPoolSize和invocationCount。
@Test(enabled=true, dataProvider="testdp", threadPoolSize=5, invocationCount=10)
其中threadPoolSize表明用于調(diào)用該方法的線程池容量,該例是同時(shí)起5個(gè)線程并行執(zhí)行該方法;invocationCount表示該方法總計(jì)需要被執(zhí)行的次數(shù)。該例子中5個(gè)線程同時(shí)執(zhí)行,當(dāng)總計(jì)執(zhí)行次數(shù)達(dá)到10次時(shí),停止。
注意,該線程池與dp的并發(fā)線程池是兩個(gè)獨(dú)立的線程池。這里的線程池是用于起多個(gè)method,而每個(gè)method的測(cè)試數(shù)據(jù)由dp提供,如果這邊dp里有3組數(shù)據(jù),那么實(shí)際上10次執(zhí)行,每次都會(huì)調(diào)3次接口,這個(gè)接口被調(diào)用的總次數(shù)是10*3=30次。threadPoolSize指定的5個(gè)線程中,每個(gè)線程單獨(dú)去調(diào)method時(shí),用到的dp如果也是支持并發(fā)執(zhí)行的話,會(huì)創(chuàng)建一個(gè)新的線程池(dpThreadPool)來(lái)并發(fā)執(zhí)行測(cè)試數(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");//設(shè)置日期格式
@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()));
}
}
測(cè)試結(jié)果:
Start Time: 2017-03-11 14:10:43
[ThreadUtil] Starting executor timeOut:0ms workers:5 threadPoolSize:2
Current Thread Id: 14. Dataprovider number: 2
Current Thread Id: 15. Dataprovider number: 2
Current Thread Id: 12. Dataprovider number: 1
Current Thread Id: 13. Dataprovider number: 1
Current Thread Id: 16. Dataprovider number: 1
Current Thread Id: 18. Dataprovider number: 1
Current Thread Id: 17. Dataprovider number: 2
Current Thread Id: 19. Dataprovider number: 2
Current Thread Id: 21. Dataprovider number: 2
Current Thread Id: 20. Dataprovider number: 1
End Time: 2017-03-11 14:10:58
Other TestNG Tips
TestNG作為一個(gè)成熟的、業(yè)界廣泛使用的測(cè)試框架,自然有其存在的合理性。這邊再分享一些簡(jiǎn)單有用的標(biāo)簽,具體的使用姿勢(shì)大家可以自己去探索,官網(wǎng)有比較全的介紹,畢竟自己探索的才會(huì)印象深刻。
1、groups/dependsOnGroups/dependsOnMethods ——設(shè)置用例間依賴
2、dataProviderClass ——將dataprovider單獨(dú)放到一個(gè)專用的類中,實(shí)現(xiàn)測(cè)試代碼、dataprovider、測(cè)試數(shù)據(jù)分層。
3、timeout ——設(shè)置用例的超時(shí)時(shí)間(并發(fā)/非并發(fā)都可支持)
4、alwaysRun ——某些依賴的用例失敗了,導(dǎo)致用例被跳過(guò)。對(duì)于一些為了保持環(huán)境干凈而“掃尾”的測(cè)試類,如果我們想強(qiáng)制執(zhí)行可以使用此標(biāo)簽。
5、priority ——設(shè)置優(yōu)先級(jí),讓某些測(cè)試用例被更大概率優(yōu)先執(zhí)行。
6、singleThreaded ——強(qiáng)制一個(gè)class類里的用例在一個(gè)線程執(zhí)行,忽視method級(jí)別并發(fā)
7、preserve-order ——指定是否按照testng.xml中的既定用例順序執(zhí)行用例
總結(jié)
在TestNG中使用多線程的方式并行執(zhí)行測(cè)試用例可以有效提供用例的執(zhí)行速度,而且TestNG對(duì)多線程提供了很好的支持,即使是菜鳥也可以方便地上手多線程。此外,TestNG默認(rèn)會(huì)使用線程池的方式創(chuàng)建線程,減小了程序的開(kāi)銷。