自动化工具 简单聊聊 TestNG 中的并发

Nisir · March 11, 2017 · Last by mengzongzhu replied at May 26, 2020 · 3687 hits
本帖已被设为精华帖!

前言

最近在做项目里的自动化测试工作,使用的是 TestNG 测试框架,主要涉及的测试类型有接口测试以及基于业务实际场景的场景化测试。由于涉及的场景大多都是大数据的作业开发及执行(如 MapReduce、Spark、Hql 等任务的执行),而这些任务的执行都需要耗费较多的时间。举一个普遍的例子,其中一条场景测试用例是:

  • 执行一个 MapReduce 作业,校验作业的执行结果和执行日志。

对于一个最简单的 MR 任务,如果 YARN 集群资源充足,它的执行时间也要花上将近一分钟的时间。更不用说当 YARN 集群计算资源饱和时,任务还需要持续等待资源分配等。
当测试回归用例集里包含了大量此类的用例时,如果还用传统的单线程执行方式,则一次自动化回归将会耗费大量的时间。

多线程并行执行

基于上述场景,我们可以考虑将自动化用例中相互之间没有耦合关系,相对独立的用例进行并行执行。如,我可以通过起不同的线程同时去执行不同的 MR 任务、Spark 任务,每个线程各自负责跟踪任务的执行情况。

此外,即使是单纯的接口自动化测试,如果测试集里包含了大量的用例时,我们也可以借助于 TestNG 的多线程方式提高执行速度。

必须要指出的是,通过多线程执行用例时虽然可以大大提升用例的执行效率,但是我们在设计用例时也要考虑到这些用例是否适合并发执行,以及要注意多线程方式的通病:线程安全与共享变量的问题。建议是在测试代码中,尽可能地避免使用共享变量。如果真的用到了,要慎用 synchronized 关键字来对共享变量进行加锁同步。否则,难免你的用例执行时可能会出现不稳定的情景(经常听到有人提到用例执行地不稳定,有时 100% 通过,有时只有 90% 通过,猜测可能有一部分原因也是这个导致的)。

TestNG 中的多线程使用姿势

不同级别的并发

通常,在 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 下设置,如:

<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 个线程去同时执行不同的用例。
它们的区别如下:

  • 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 多组测试数据之间的并发。执行时会发现,一个 dp 中的多组数据依然是顺序执行。

解决方式是:在@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 并行执行的线程池容量为 10,如果要更改并发的数量,也可以在 suite tag 下指定参数 data-provider-thread-count:

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

同一个方法的并发

有些时候,我们需要对一个测试用例,比如一个 http 接口,执行并发测试,即一个接口的反复调用。TestNG 中也提供了优雅的支持方式,在@Test标签中指定 threadPoolSize 和 invocationCount。

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

其中 threadPoolSize 表明用于调用该方法的线程池容量,该例就是同时起 5 个线程并行执行该方法;invocationCount 表示该方法总计需要被执行的次数。该例子中 5 个线程同时执行,当总计执行次数达到 10 次时,停止。

注意,该线程池与 dp 的并发线程池是两个独立的线程池。这里的线程池是用于起多个 method,而每个 method 的测试数据由 dp 提供,如果这边 dp 里有 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()));
    }
}

测试结果:

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 作为一个成熟的、业界广泛使用的测试框架,自然有其存在的合理性。这边再分享一些简单有用的标签,具体的使用姿势大家可以自己去探索,官网有比较全的介绍,毕竟自己探索的才会印象深刻。

  1. groups/dependsOnGroups/dependsOnMethods ——设置用例间依赖
  2. dataProviderClass ——将 dataprovider 单独放到一个专用的类中,实现测试代码、dataprovider、测试数据分层。
  3. timeout ——设置用例的超时时间(并发/非并发都可支持)
  4. alwaysRun ——某些依赖的用例失败了,导致用例被跳过。对于一些为了保持环境干净而 “扫尾” 的测试类,如果我们想强制执行可以使用此标签。
  5. priority ——设置优先级,让某些测试用例被更大概率优先执行。
  6. singleThreaded ——强制一个 class 类里的用例在一个线程执行,忽视 method 级别并发
  7. preserve-order ——指定是否按照 testng.xml 中的既定用例顺序执行用例

总结

在 TestNG 中使用多线程的方式并行执行测试用例可以有效提供用例的执行速度,而且 TestNG 对多线程提供了很好的支持,即使是菜鸟也可以方便地上手多线程。此外,TestNG 默认会使用线程池的方式创建线程,减小了程序的开销。

参考链接

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 18 条回复 时间 点赞
恒温 将本帖设为了精华贴 11 Mar 21:58
匿名 #3 · March 13, 2017

风哥赞👍

Nisir #4 · March 13, 2017 Author

你是。。??

匿名 #5 · March 13, 2017
Nisir 回复

yyan.qiang😂

讲的很详细和清晰,受益匪浅

Nisir #8 · March 14, 2017 Author

多谢支持

样式很漂亮。内容很详细,收藏了

😀 😀 风哥 6 的不行

Nisir #11 · March 15, 2017 Author

#10 楼 @lihe1986 李赫。。。

—— 来自 TesterHome 官方 安卓客户端

记得更新打赏二维码

Nisir #13 · March 20, 2017 Author

hoho, 已更新。

风哥好 6 啊

Nisir #15 · March 21, 2017 Author
P_Oliver 回复

哪位哈。。

Nisir 我对自动化测试的一些认识 中提及了此贴 09 Aug 11:32

Nisir,您好,想问下并发执行日志怎么进行管理,日志输出会比较乱,有没有方法针对设备进行区分

Nisir #18 · December 07, 2019 Author
greenplum 回复

老帖竟然被挖出来了。。。你好,并发下的日志输出比较乱,这个你可以将有效日志行的格式按照固定的结构打印输出即可。针对设备进行区分,可以在日志行中增加一列或者一个字段标识

greenplum 回复

控制台输出的日志是乱的,用 log4j 管理日志,输出日志到 log 文件里,或者 testNG Reporter 来记录日志展现到测试报告,可以完美解决这个问题。

冒昧的提一点,楼主的这种多线程并发执行用例的方法,是不适用 UI 自动化测试的。UI 自动化必须是一个线程实例化一个 webdriver 实例,线程之间不能共享 webdriver 实例,否则用例执行会串,各种奇葩问题会抛出来。

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up