简单聊聊 TestNG 中的并发

ws2571 3年前
   <h2>前言</h2>    <p>最近在做项目里的自动化测试工作,使用的是TestNG测试框架,主要涉及的测试类型有接口测试以及基于业务实际场景的场景化测试。由于涉及的场景大多都是大数据的作业开发及执行(如MapReduce、Spark、Hql等任务的执行),而这些任务的执行都需要耗费较多的时间。举一个普遍的例子,其中一条场景测试用例是:</p>    <ul>     <li>执行一个MapReduce作业,校验作业的执行结果和执行日志。</li>    </ul>    <p>对于一个最简单的MR任务,如果YARN集群资源充足,它的执行时间也要花上将近一分钟的时间。更不用说当YARN集群计算资源饱和时,任务还需要持续等待资源分配等。</p>    <p>当测试回归用例集里包含了大量此类的用例时,如果还用传统的单线程执行方式,则一次自动化回归将会耗费大量的时间。</p>    <h2>多线程并行执行</h2>    <p>基于上述场景,我们可以考虑将自动化用例中相互之间没有耦合关系,相对独立的用例进行并行执行。如,我可以通过起不同的线程同时去执行不同的MR任务、Spark任务,每个线程各自负责跟踪任务的执行情况。</p>    <p>此外,即使是单纯的接口自动化测试,如果测试集里包含了大量的用例时,我们也可以借助于TestNG的多线程方式提高执行速度。</p>    <p>必须要指出的是,通过多线程执行用例时虽然可以大大提升用例的执行效率,但是我们在设计用例时也要考虑到这些用例是否适合并发执行,以及要注意多线程方式的通病:线程安全与共享变量的问题。建议是在测试代码中,尽可能地避免使用共享变量。如果真的用到了,要慎用synchronized关键字来对共享变量进行加锁同步。否则,难免你的用例执行时可能会出现不稳定的情景(经常听到有人提到用例执行地不稳定,有时100%通过,有时只有90%通过,猜测可能有一部分原因也是这个导致的)。</p>    <h2>TestNG中的多线程使用姿势</h2>    <h2>不同级别的并发</h2>    <p>通常,在TestNG的执行中,测试的级别由上至下可以分为 <strong>suite -> test -> class -> method</strong> ,箭头的左边元素跟右边元素的关系是一对多的包含关系。</p>    <p>这里的test指的是testng.xml中的test tag,而不是测试类里的一个 <em>@</em> Test 。测试类里的一个 <em>@</em> Test 实际上对应这里的method。所以我们在使用 <em>@</em> BeforeSuite 、 <em>@</em> BeforeTest 、 <em>@</em> BeforeClass 、 <em>@</em> BeforeMethod 这些标签的时候,它们的实际执行顺序也是按照这个级别来的。</p>    <h3>suite</h3>    <p>一般情况下,一个testng.xml只包含一个suite。如果想起多个线程执行不同的suite,官方给出的方法是:通过命令行的方式来指定线程池的容量。</p>    <pre>  <code class="language-java">java org.testng.TestNG -suitethreadpoolsize 3 testng1.xml testng2.xml testng3.xml</code></pre>    <p>即可通过三个线程来分别执行testng1.xml、testng2.xml、testng3.xml。</p>    <p>实际上这种情况在实际中应用地并不多见,我们的测试用例往往放在一个suite中,如果真需要执行不同的suite,往往也是在不同的环境中去执行,届时也自然而然会做一些其他的配置(如环境变量)更改,会有不同的进程去执行。因此这种方式不多赘述。</p>    <h3>test, class, method</h3>    <p>test,class,method级别的并发,可以通过在testng.xml中的suite tag下设置,如:</p>    <pre>  <code class="language-java"><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"></code></pre>    <p>它们的共同点都是最多起5个线程去同时执行不同的用例。</p>    <p>它们的区别如下:</p>    <ul>     <li>tests级别:不同test tag下的用例可以在不同的线程执行,相同test tag下的用例只能在同一个线程中执行。</li>     <li>classs级别:不同class tag下的用例可以在不同的线程执行,相同class tag下的用例只能在同一个线程中执行。</li>     <li>methods级别:所有用例都可以在不同的线程去执行。</li>    </ul>    <p>搞清楚并发的级别非常重要,可以帮我们合理地组织用例,比如将非线程安全的测试类或group统一放到一个test中,这样在并发的同时又可以保证这些类里的用例是单线程执行。也可以根据需要设定class级别的并发,让同一个测试类里的用例在同一个线程中执行。</p>    <h2>并发时的依赖</h2>    <p>实践中,很多时候我们在测试类中通过dependOnMethods/dependOnGroups方式,给很多测试方法的执行添加了依赖,以达到期望的执行顺序。如果同时在运行testng时配置了methods级别并发执行,那么这些测试方法在不同线程中执行,还会遵循依赖的执行顺序吗?答案是——YES。牛逼的TestNG就是能在多线程情况下依然遵循既定的用例执行顺序去执行。</p>    <h2>不同dataprovider的并发</h2>    <p>在使用TestNG做自动化测试时,基本上大家都会使用dataprovider来管理一个用例的不同测试数据。而上述在testng.xml中修改suite标签的方法,并不适用于dataprovider多组测试数据之间的并发。执行时会发现,一个dp中的多组数据依然是顺序执行。</p>    <p>解决方式是:在 <em>@</em> DataProvider 中添加parallel=true。</p>    <p>如:</p>    <pre>  <code class="language-java">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          };      }  }</code></pre>    <p>默认情况下,dp并行执行的线程池容量为10,如果要更改并发的数量,也可以在suite tag下指定参数data-provider-thread-count:</p>    <pre>  <code class="language-java"><suite name="Testng Parallel Test" parallel="methods" thread-count="5" data-provider-thread-count="20" ></code></pre>    <h2>同一个方法的并发</h2>    <p>有些时候,我们需要对一个测试用例,比如一个http接口,执行并发测试,即一个接口的反复调用。TestNG中也提供了优雅的支持方式,在 <em>@</em> Test 标签中指定threadPoolSize和invocationCount。</p>    <pre>  <code class="language-java">@Test(enabled=true, dataProvider="testdp", threadPoolSize=5, invocationCount=10)</code></pre>    <p>其中threadPoolSize表明用于调用该方法的线程池容量,该例就是同时起5个线程并行执行该方法;invocationCount表示该方法总计需要被执行的次数。该例子中5个线程同时执行,当总计执行次数达到10次时,停止。</p>    <p>注意,该线程池与dp的并发线程池是两个独立的线程池。这里的线程池是用于起多个method,而每个method的测试数据由dp提供,如果这边dp里有3组数据,那么实际上10次执行,每次都会调3次接口,这个接口被调用的总次数是10*3=30次。threadPoolSize指定的5个线程中,每个线程单独去调method时,用到的dp如果也是支持并发执行的话,会创建一个新的线程池(dpThreadPool)来并发执行测试数据。</p>    <p>示例代码如下:</p>    <pre>  <code class="language-java">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()));      }  }</code></pre>    <p>测试结果:</p>    <pre>  <code class="language-java">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</code></pre>    <h2>Other TestNG Tips</h2>    <p>TestNG作为一个成熟的、业界广泛使用的测试框架,自然有其存在的合理性。这边再分享一些简单有用的标签,具体的使用姿势大家可以自己去探索,官网有比较全的介绍,毕竟自己探索的才会印象深刻。</p>    <ol>     <li>groups/dependsOnGroups/dependsOnMethods ——设置用例间依赖</li>     <li>dataProviderClass ——将dataprovider单独放到一个专用的类中,实现测试代码、dataprovider、测试数据分层。</li>     <li>timeout ——设置用例的超时时间(并发/非并发都可支持)</li>     <li>alwaysRun ——某些依赖的用例失败了,导致用例被跳过。对于一些为了保持环境干净而“扫尾”的测试类,如果我们想强制执行可以使用此标签。</li>     <li>priority ——设置优先级,让某些测试用例被更大概率优先执行。</li>     <li>singleThreaded ——强制一个class类里的用例在一个线程执行,忽视method级别并发</li>     <li>preserve-order ——指定是否按照testng.xml中的既定用例顺序执行用例</li>    </ol>    <h2>总结</h2>    <p>在TestNG中使用多线程的方式并行执行测试用例可以有效提供用例的执行速度,而且TestNG对多线程提供了很好的支持,即使是菜鸟也可以方便地上手多线程。此外,TestNG默认会使用线程池的方式创建线程,减小了程序的开销。</p>    <h2>参考链接</h2>    <ul>     <li><a href="/misc/goto?guid=4959741215616384675" rel="nofollow,noindex">TestNG Documentation</a></li>    </ul>    <p> </p>    <p>来自:https://testerhome.com/topics/7895</p>    <p> </p>