TestNG是一个开源自动化测试框架,表示下一代(Next Generation的首字母),其设计思想来自JUnit和NUnit,但引入了一些新的创新功能,如依赖测试,分组概念,使测试更强大,更容易做到。 它旨在涵盖所有类别的测试:单元测试,功能测试,端到端的测试,集成测试等。
两种框架在功能上看起来非常相似,主要有以下3点区别:分组测试,数据驱动和依赖测试。
|
注解支持 |
忽略测试 |
超时测试 |
套件测试 |
异常测试 |
分组测试 |
参数化支持 |
数据驱动 |
依赖测试 |
---|---|---|---|---|---|---|---|---|---|
JUnit |
√ |
√ |
√ |
√ |
√ |
× |
√ |
× |
× |
TestNG |
√ |
√ |
√ |
√ |
√ |
√ |
√ |
√ |
√ |
分组测试:
凭借TestNG独特的“分组”概念,每种方法都可以与一个组合相结合,可以根据功能对测试进行分类(分组)。 例如,
下面是一个有四个方法的类,三个组(method1,method2和method3)
@Test(groups="method1")
public void testingMethod1() {
System.out.println("Method - testingMethod1()");
}
@Test(groups="method2")
public void testingMethod2() {
System.out.println("Method - testingMethod2()");
}
@Test(groups="method1")
public void testingMethod1_1() {
System.out.println("Method - testingMethod1_1()");
}
@Test(groups="method3")
public void testingMethod3() {
System.out.println("Method - testingMethod3()");
}
使用以下XML文件,可以仅使用组“method1”执行测试。
数据驱动:
目前在各个项目的接口测试中都在广泛使用,不展开说明了。
依赖测试:
如果依赖方法失败,则所有后续测试将会被跳过,不会被标记为失败。JUnit框架着重于测试隔离,目前不支持此功能。
TestNG使用“dependOnMethods”注解来实现依赖测试。
@Test
public void method1() {
System.out.println("This is method 1");
}
@Test(dependsOnMethods={"method1"})
public void method2() {
System.out.println("This is method 2");
}
“method2()”只有在“method1()”运行成功的情况下才会执行,否则“method2()”将跳过测试。
TestNG是一个基于注解的测试框架。“注解”是什么东西?TestNG有哪些常用注解,它们的生命周期分别是什么?
Java中的注解(Annotation),也叫元数据,是一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。
Testng的14个常用注解及其生命周期:
注解 |
描述 |
@BeforeSuite |
在该套件的所有测试都运行在注释的方法之前,仅运行一次。 |
@AfterSuite |
在该套件的所有测试都运行在注释方法之后,仅运行一次。 |
@BeforeClass |
在调用当前类的第一个测试方法之前运行,注释方法仅运行一次。 |
@AfterClass |
在调用当前类的第一个测试方法之后运行,注释方法仅运行一次 |
@BeforeTest |
注释的方法将在属于 |
@AfterTest |
注释的方法将在属于 |
@BeforeGroups |
此配置方法将在之前运行组列表。 此方法保证在调用属于这些组中的任何一个的第一个测试方法之前不久运行。 |
@AfterGroups |
此配置方法将在之后运行组列表。该方法保证在调用属于任何这些组的最后一个测试方法之后不久运行。 |
@BeforeMethod |
注释方法将在每个测试方法(@Text注解)之前运行。 |
@AfterMethod |
注释方法将在每个测试方法之后运行。 |
@DataProvider |
标记一种方法来提供测试方法的数据。 注释方法必须返回一个Object [] [],其中每个Object []可以被分配给测试方法的参数列表。 要从该DataProvider接收数据的@Test方法需要使用与此注释名称相等的dataProvider名称。 |
@Listeners |
定义测试类上的侦听器。后面会介绍 |
@Parameters |
描述如何将参数传递给@Test方法。 |
@Test |
将类或方法标记为测试的一部分。 |
最后4个注解链接了源码,有兴趣的同学可以自行下载查看。
不侮辱大家的智商,跳过TestNG和插件的下载、安装环节(官网下载地址)。使用testng执行测试有两种方法:
第一种方法,直接执行右键要执行的方法或类,选择Run As ->TestNG Test
第二种方法,通过testng.xml文件来执行。 把要执行的case, 配置到testng.xml文件中。 右键testng.xml -> Run As...
TestNG默认的执行顺序是case的字母序,在testng.xml中,可以控制测试用例的执行顺序, 当preserve-order="true"是,可以保证节点下面的方法是按顺序执行的。
参数化测试和数据驱动
软件测试中,经常需要测试大量的数据集。 测试代码的逻辑完全一样,只是测试的参数不一样。 我们就需要一种 “传递测试参数的机制”,避免写重复的测试代码。
TestNG提供了2种传递参数的方式。
第一种: testng.xml 方式,使代码和测试数据分离,方便维护。
package super.qa;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;
public class ParameterizedTest1 {
@Test
@Parameters("test1")
public void ParaTest(String test1){
System.out.println("This is " + test1);
}
}
testng.xml:
目前质量组的app自动化框架中,AppiumDriver的启动参数就是通过这种方式“传递”的。
我们都知道,一个suite的每个test tag都是针对一个单独测试集,两个test中的测试方法不会相互影响。但是在这两种测试范围定义的参数,会产生相互影响吗?
有以下三个原则:
1)在Suite范围内定义某个参数的值,对该Suite下所有的Test都有效。
2)在Test范围内定义某个参数的值,只是针对该Test范围有效。
3)如果同时在Suite和Test中定义某个同名的参数,Test范围的值会屏蔽Suite的值。
第二种参数传递的方式:通过@DataProvider提供比较复杂的参数,也叫数据驱动测试。我们目前的接口测试已经在用了,不展开说了。
作为补充,对TestNG的一些比较有用的用法做一下总结:
dependsOnGroups/dependsOnMethods ——设置用例间依赖。
dataProviderClass ——将dataprovider单独放到一个专用的类中,实现测试代码、dataprovider、测试数据分层。一般的接口测试框架就是这么干的。
timeout ——设置用例的超时时间(并发/非并发都可支持)。
alwaysRun ——用来避免因为某些依赖的用例失败导致用例被跳过。对于一些为了保持环境干净而“扫尾”的测试类,如果我们想强制执行可,以使用此标签。
priority ——设置优先级,让某些测试用例被更大概率优先执行。
singleThreaded ——强制一个class类里的用例在一个线程执行,忽视method级别并发。
preserve-order ——指定是否按照testng.xml中的指定的用例顺序执行。
监听器是一个专门用于对其他对象身上发生的事件或状态改变进行监听和做出相应处理的对象,当被监视的对象发生情况时,立即采取相应的行动。通俗地说,TestNg的监听机制类似安卓的BroadcaseRecevier和MFC的消息机制,监听器的编码过程就是定义一个 Java 类实现监听器接口org.testng.ITestNGListener。TestNg中提供的一些常用的监听器都是通过实现org.testng.ITestNGListener接口而完成的,下面简单介绍一下监听器的两种使用方法。
使用注解或者通过testng.xml 定义TestNG 监听器
Testng提供了 listeners 和 listener 标签用来添加自定义的监听器,可以直接在 Java 代码中添加 @Listeners 注解编写 TestNG 监听器。
@Listeners({ OSFilter.class, ProgressTracker.class })
public class SampleTest {
@Test(groups = { OSNames.OS_LINUX })
public void test1() {
sleep(5000);
System.out.println(">>>test1");
}
}
值得注意的是:
在 @Listeners 中添加监听器跟在 testng.xml 添加监听器一样,将被应用到整个测试套件中的所有测试方法。如果需要控制监听器的应用范围(比如添加的监听器仅使用于某些测试测试类或者某些测试方法),则必须在监听器类中自己编写适当的判断逻辑。
在 @Listeners 中添加监听器跟在 testng.xml 添加监听器的不同之处在于,它不能添加IAnnotationTransformer和IAnnotationTransformer2监听器(这两个监听器的作用都是在TestNG执行过程中动态改变测试类中Annotation的参数)。原因是因为这两种监听器必须在更早的阶段添加到 TestNG 中才能实施修改注解的操作,所以它们只能在 testng.xml 添加。 TestNG 对添加的监听器不做去重判断。因此,如果 testng.xml 和源代码中添加了相同的监听器,该监听器的方法会被调用两次。因此,如非必要,不要通过多种方式重复添加监听器。
通过 ServiceLoader 使用 TestNG 监听器
JDK 6 开始提供了 ServiceLoader(注意和ClassLoader的区别),它可以帮助用户查找、加载和使用服务提供程序,在无需修改原有代码的情况下轻易地扩展目标应用程序。通过 ServiceLoader 的方式使用 TestNG 监听器,简单来说,就是创建一个 jar 文件,里面包含 TestNG 监听器的实现类已经 ServiceLoader 需要的配置信息,并在运行 TestNG 时把该 jar 文件加载到类路径中。这样做的好处是:
可以轻松地与其他人分享 TestNG 监听器。
当有很多 testng.xml 文件时,不需要重复把监听器添加到每个文件中。
通过 ServiceLoader 使用 TestNG 监听器,需要通过命令行启动,在命令行中加入”-listener”参数。如要指定多个监听器,用逗号分隔。
java org.testng.TestNG -listener MyListener testng1.xml [testng2.xml testng3.xml ...]
用例失败重试/自动截图
实现 IRetryAnalyzer接口
package ec.qa.autotest.ui.testng.listener;
import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;
import org.testng.Reporter;
import ec.qa.autotest.ui.constants.CommonConstants;
import ec.qa.autotest.ui.testbase.TestBase;
public class RetryToRunCase implements IRetryAnalyzer{
private int retryCount = 1;
private static int maxRetryCount;
public int getRetryCount() {
return retryCount;
}
public static int getMaxRetryCount() {
return maxRetryCount;
}
@SuppressWarnings("static-access")
public RetryToRunCase(){
this.maxRetryCount = CommonConstants.RETRY_COUNT;
}
public boolean retry(ITestResult result) {
if (retryCount <= maxRetryCount) {
Reporter.setCurrentTestResult(result);
TestBase.success = false;
retryCount++;
return true;
}
return false;
}
}
实现testNg监听器的2个接口
package ec.qa.autotest.ui.testng.listener;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.testng.IAnnotationTransformer;
import org.testng.IRetryAnalyzer;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;
import org.testng.annotations.ITestAnnotation;
public class TestngRetryListener implements IAnnotationTransformer,ITestListener {
@SuppressWarnings("rawtypes")
public void transform(ITestAnnotation annotation, Class testClass, Constructor testConstructor, Method testMethod) {
IRetryAnalyzer retry = annotation.getRetryAnalyzer();
if (retry == null) {
annotation.setRetryAnalyzer(RetryToRunCase.class);
}
}
public void onFinish(ITestContext testContext) {
ArrayList testsToBeRemoved = new ArrayList();
Set passedTestIds = new HashSet();
for (ITestResult passedTest : testContext.getPassedTests().getAllResults()) {
passedTestIds.add(getId(passedTest));
}
Set failedTestIds = new HashSet();
for (ITestResult failedTest : testContext.getFailedTests().getAllResults()) {
int failedTestId = getId(failedTest);
if (failedTestIds.contains(failedTestId) || passedTestIds.contains(failedTestId)) {
testsToBeRemoved.add(failedTest);
} else {
failedTestIds.add(failedTestId);
}
}
for (Iterator iterator = testContext.getFailedTests().getAllResults().iterator(); iterator
.hasNext();) {
ITestResult testResult = iterator.next();
if (testsToBeRemoved.contains(testResult)) {
iterator.remove();
}
}
}
private int getId(ITestResult result) {
int id = result.getTestClass().getName().hashCode();
id = id + result.getMethod().getMethodName().hashCode();
id = id + (result.getParameters() != null ? Arrays.hashCode(result.getParameters()) : 0);
return id;
}
public void onTestFailure(ITestResult result) {
}
public void onTestSuccess(ITestResult result) {
// TODO Auto-generated method stub
}
//...
}
onFinish()方法的代码是为了在测试结果报告中剔除掉失败后重试成功的结果。
在tesng.xml中配置上述testng监听器,略
自定义测试报告
TestNG默认情况下,会产生不同类型的测试执行报告,包括HTML和XML格式的。此外, TestNG的还允许用户自定义测试报告。
主要有两种方法来生成自定义报告:
监听器方法
实现IReporter接口
监听器的使用前面有过介绍,下面介绍一下基于IReporter接口,实现自定义测试报告的方法:
创建测试类
import org.testng.Assert;
import org.testng.annotations.Test;
public class SampleTest {
@Test
public void testMethodOne(){
Assert.assertTrue(true);
}
@Test
public void testMethodTwo(){
Assert.assertTrue(false);
}
@Test(dependsOnMethods={"testMethodTwo"})
public void testMethodThree(){
Assert.assertTrue(true);
}
}
上述测试类的包含三个测试方法,其中testMethodOne 和 testMethodThree将通过在执行时,而testMethodTwo由通过一个falseBoolean的值Assert.assertTrue方法,它是用于在测试中的真值条件失败。
创建自定义报告类
创建一个类 CustomReporter.java 实现IReporter接口,通过重写IReporter接口定义的方法GenerateReport来实现自定义测试报告的功能。
import java.util.List;
import java.util.Map;
import org.testng.IReporter;
import org.testng.ISuite;
import org.testng.ISuiteResult;
import org.testng.ITestContext;
import org.testng.xml.XmlSuite;
public class CustomReporter implements IReporter{
@Override
public void generateReport(List xmlSuites, List suites, String outputDirectory) {
//Iterating over each suite included in the test
for (ISuite suite : suites) {
//Following code gets the suite name
String suiteName = suite.getName();
//Getting the results for the said suite
Map suiteResults = suite.getResults();
for(ISuiteResult sr : suiteResults.values()) {
ITestContext tc = sr.getTestContext();
System.out.println("Passed tests for suite '" + suiteName + "' is:" + tc.getPassedTests().getAllResults().size());
System.out.println("Failed tests for suite '" + suiteName + "' is:" + tc.getFailedTests().getAllResults().size());
System.out.println("Skipped tests for suite '" + suiteName + "' is:" + tc.getSkippedTests().getAllResults().size());
}
}
}
}
generateReport这个方法有三个参数:
第一个是xmlSuites,这是TestNG的xml配置文件中所包含的测试套件。
第二个是测试套件,包含了测试执行后的全部信息(包,类,测试方法和测试执行结果),这也是最重要的参数。
第三的outputDirectory,报告将存放的文件夹路径。
创建 testng.xml
创建一个文件testng.xml 来管理和执行测试用例
执行测试,验证输出
=============================================== Simple Reporter Suite Total tests run: 3, Failures: 1, Skips: 1 =============================================== Passed tests for suite 'Simple Reporter Suite' is:1 Failed tests for suite 'Simple Reporter Suite' is:1 Skipped tests for suite 'Simple Reporter Suite' is:1
在使用TestNG执行测试的时候,难免会遇到耗时比较长的操作,我们可以考虑将自动化用例中相互之间没有耦合关系,相对独立的用例进行并行执行。即使是单纯的接口自动化测试,如果测试集里包含了大量的用例时,我们也可以借助于TestNG的多线程方式提高执行速度。
必须要指出的是,通过多线程执行用例时虽然可以大大提升用例的执行效率,但是我们在设计用例时也要考虑到这些用例是否适合并发执行,以及要注意多线程方式的通病:线程安全与共享变量的问题。建议是在测试代码中,尽可能地避免使用共享变量。如果真的用到了,要慎用synchronized关键字来对共享变量进行加锁同步。否则,难免你的用例执行时可能会出现不稳定的情景(经常听到有人提到用例执行地不稳定,有时100%通过,有时只有90%通过,猜测可能有一部分原因也是这个导致的)。
不同级别的并发
通常,在TestNG的执行中,测试的级别由上至下可以分为suite -> test -> class -> method,箭头的左边元素跟右边元素的关系是一对多的包含关系。
这里的test指的是testng.xml中配置的test tag,而不是测试类里的一个 @Test。测试类里的一个 @Test实际上对应这里的method。所以我们在使用 @BeforeSuite、 @BeforeTest、 @BeforeClass、 @BeforeMethod这些标签的时候,它们的实际执行顺序也是按照这个级别来的。
suite
一般情况下,一个testng.xml只包含一个suite。如果想起多个线程执行不同的suite,官方给出的方法是:通过命令行的方式来指定线程池的容量。
java org.testng.TestNG -suitethreadpoolsize 3 testng1.xml testng2.xml testng3.xml
即可通过三个线程来分别执行testng1.xml、testng2.xml、testng3.xml。
实际上这种情况在实际中应用地并不多见,我们的测试用例往往放在一个suite中,如果真需要执行不同的suite,往往也是在不同时机或者在不同的环境中去执行,届时也自然而然会做一些其他的配置(如环境变量)更改,会有不同的进程去执行,因此这种方式不多赘述。
test, class, method
test,class,method级别的并发,可以通过在testng.xml中的suite tag下设置,如:
需要注意的是,上述代码块中的parallel的值不是设置成“true”,而是需要并发的测试级别!
它们的共同点都是最多起5个线程去同时执行不同的用例。
它们的区别如下:
tests级别:不同test tag下的用例可以在不同的线程执行,相同test tag下的用例只能在同一个线程中执行。
classs级别:不同class tag下的用例可以在不同的线程执行,相同class tag下的用例只能在同一个线程中执行。
methods级别:所有用例都可以在不同的线程去执行。
搞清楚并发的级别非常重要,可以帮我们合理地组织用例,比如将非线程安全的测试类或group统一放到一个test中,这样在并发的同时又可以保证这些类里的用例是单线程执行。也可以根据需要设定class级别的并发,让同一个测试类里的用例在同一个线程中执行。
并发时的依赖
实践中,很多时候我们在测试类中通过dependOnMethods/dependOnGroups方式,给很多测试方法的执行添加了依赖,以达到期望的执行顺序。如果同时在运行testng时配置了methods级别并发执行,那么这些测试方法在不同线程中执行,还会遵循依赖的执行顺序吗?答案是——YES。TestNG就是能在多线程情况下依然遵循既定的用例执行顺序去执行。
不同dataprovider的并发
在使用TestNG做自动化测试时,基本上大家都会使用dataprovider来管理一个用例的不同测试数据。而上述在testng.xml中修改suite标签的方法,并不适用于dataprovider多组测试数据之间的并发。执行时会发现,一个dataprovider中的多组数据依然是顺序执行。
解决方式是:在 @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
};
}
}
默认情况下,dataprovider并行执行的线程池容量为10,如果要更改并发的数量,也可以在suite tag下指定参数data-provider-thread-count:
同一个方法的并发
有些时候,我们需要对一个测试用例,比如一个http接口,执行并发测试,即一个接口的反复调用。TestNG中也提供了优雅的支持方式,在 @Test标签中指定threadPoolSize和invocationCount。
@Test(enabled=true, dataProvider="testdp", threadPoolSize=5, invocationCount=10)
其中threadPoolSize表明用于调用该方法的线程池容量,该例就是同时起5个线程并行执行该方法;invocationCount表示该方法总计需要被执行的次数。该例子中5个线程同时执行,当总计执行次数达到10次时,停止。
注意,该线程池与dataprovider的并发线程池是两个独立的线程池。这里的线程池是用于起多个method,而每个method的测试数据由dp提供,如果这边dataprovider里有3组数据,那么实际上10次执行,每次都会调3次接口,这个接口被调用的总次数是10*3=30次。threadPoolSize指定的5个线程中,每个线程单独去调method时,用到的dp如果也是支持并发执行的话,会创建一个新的线程池(dpThreadPool)来并发执行测试数据。
示例代码如下:
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()));
}
}
测试结果:
wStart Time: 2018-09-24 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: 2018-09-24 14:10:58
两个问题:
QA的接口测试为什么一般不选用Junit框架,而要用TestNG?
TestNG中使用多线程并发执行时,怎么控制case的执行顺序?
两个作业(自愿完成):
基于ServiceLoader使用监听机制,在目前的项目中实现一个testng的自定义报告
在本地使用多线程执行实际项目的接口测试代码,处理好并发时的顺序和依赖,并找出并发的最佳线程数(时间开销最短)
附录:
TestNG官方文档 http://testng.org/doc/documentation-main.html