Java + TestNG + Appium 实现单机多个Android终端并发测试
来源:互联网 发布:几个淘宝号刷一天挣40 编辑:程序博客网 时间:2024/05/01 10:06
前言
我们知道,单台 PC 上用 Appium 连接多个 Android 终端进行测试时,需要同时用不同的端口号启动不同的 Appium Server,例如启动两个服务器:
node main.js -p 4723 -bp 4724 -chromedriver-port 9515 -U emulator1node main.js -p 4725 -bp 4726 -chromedriver-port 9516 -U emulator2
然后测试代码的 AppiumDriver
连接到对应的端口,然后就可以并发地执行测试。
这就意味着有很多环境数据需要配置,包括各种端口号,终端的UDID等,同时也需要为不同的终端分发测试用例,这些数据最好是与 Java 测试代码分离。另外服务器的启动和关闭最好也能通过代码自动执行。
之前我用的是 Junit4 单元测试框架,感觉功能不够强大,难以实现上述要求,后来了解了下 TestNG 框架,它自带的 xml 方式,参数化测试,并发执行等功能,刚好可以用来实现我设想的功能。
完整代码地址: https://github.com/zhongchenyu/AppiumTest ,下面就来介绍一下思路。
效果展示
首先看下最终实现的效果,只需配置好测试套的 xml 文件,将环境信息作为参数,指定好要执行的用例,执行 RunSuite 类,即可完成从启动服务器开始的所有测试过程。
1. 配置测试套 xml 文件,每个xml对应一个终端:
环境参数通过 parameter 标签配置,其中包含:
node:node.exe 的路径,如果配置了系统环境变量,直接填 node 就行。
appium.js:appium.js的路径,新版本的Appium应该是main.js的路径。和node参数配合,用来执行启动Appium 服务器。
port、bootstrap_port、chromedriver_port:Appium服务器的端口。
udid:终端名称,可以通过 adb devices
查到。
剩下的参数为 DesiredCapabilities 需要的参数。
第一个终端 testng1.xml
:
<suite name="WebViewSuit1" > <parameter name="suitName" value="WebViewSuit1"/> <parameter name="node" value="node"/> <parameter name="appium.js" value="C:\Users\chenyu\AppData\Local\Programs\appium-desktop\resources\app\node_modules\appium\build\lib\main.js"/> <parameter name="port" value="4725"/> <parameter name="bootstrap_port" value="4726"/> <parameter name="chromedriver_port" value="9516"/> <parameter name="udid" value="127.0.0.1:21503"/> <parameter name="platformName" value="Android"/> <parameter name="platformVersion" value="4.4.4"/> <parameter name="deviceName" value="127.0.0.1:21503"/> <parameter name="appPackage" value="chenyu.memorydemo"/> <parameter name="appActivity" value=".MainActivity"/> <parameter name="noReset" value="false"/> <parameter name="app" value="chenyu.memorydemo-debug-v1.2.apk"/> <test name="WebView"> <classes> <class name="main.java.test.TestWebView"/> </classes> </test> <test name="Animation"> <classes> <class name="main.java.test.TestAnimation"/> </classes> </test></suite>
第二个终端 testng2.xml
:
<suite name="WebViewSuit2" > <parameter name="suitName" value="WebViewSuit2"/> <parameter name="node" value="node"/> <parameter name="appium.js" value="C:\Users\chenyu\AppData\Local\Programs\appium-desktop\resources\app\node_modules\appium\build\lib\main.js"/> <parameter name="port" value="4723"/> <parameter name="bootstrap_port" value="4724"/> <parameter name="chromedriver_port" value="9515"/> <parameter name="udid" value="emulator-5554"/> <parameter name="platformName" value="Android"/> <parameter name="platformVersion" value="6.0"/> <parameter name="deviceName" value="emulator-5554"/> <parameter name="appPackage" value="chenyu.memorydemo"/> <parameter name="appActivity" value=".MainActivity"/> <parameter name="noReset" value="false"/> <parameter name="app" value="chenyu.memorydemo-debug-v1.2.apk"/> <test name="WebView"> <classes> <class name="main.java.test.TestWebView"/> </classes> </test> <test name="Animation"> <classes> <class name="main.java.test.TestAnimation"/> </classes> </test></suite>
连接好两个Android终端(模拟器或者真机),运行RunSuite
类:
即可启动并发测试,自动启动两个Appium服务器,自动执行完测试套,之后停止服务器:
实现原理
1. RunSuite 类
package main.java;import org.testng.TestListenerAdapter;import org.testng.TestNG;import java.util.ArrayList;import java.util.List;public class RunSuite { public static void main(String[] args) { TestListenerAdapter tla = new TestListenerAdapter(); TestNG testng = new TestNG(); List<String> testFieldList = new ArrayList<>(); //testFieldList.add("testng_main.xml"); testFieldList.add("testng1.xml"); testFieldList.add("testng2.xml"); testng.setTestSuites(testFieldList); testng.addListener(tla); testng.setSuiteThreadPoolSize(2); testng.run(); System.out.println("ConfigurationFailures: "+tla.getConfigurationFailures()); System.out.println("FailedTests: " + tla.getFailedTests()); }}
这个类比较简单,做的主要是加载了 TestSuite 的 xml 文件,然后并发执行多个测试套,用到的都是TestNG自带的库。
加载 xml 有两种方式,一是单独添加各个 TestSuite 的 xml 文件:
testFieldList.add("testng1.xml");testFieldList.add("testng2.xml");
或者先创建一个汇总的 xml 文件,把各个TestSuite放到其中,在加载一次汇总的文件即可。
testng_main.xml :
<suite name="Main suite"> <suite-files> <suite-file path="testng1.xml"/> <suite-file path="testng2.xml"/> </suite-files></suite>
RunSuite.java 中:
testFieldList.add("testng_main.xml");
另外要注意设置线程池大小,不设的话只有一个线程,就不会并发执行了,这里有两个 TestSuite,可以设置成2:
testng.setSuiteThreadPoolSize(2);
2. AppiumTestCase 类
AppiumTestCase 将作为所有TestCase的基类,其中包含了 Appium 服务器的启动和停止,以及 AppiumDriver 的连接和退出。
2.1 Appium Server 的启动和停止
因为一个 TestSuite 对应一个终端,一个终端对应一个Server,因此Server只需要在每个 TestSuite 开始时启动,在 TestSuite 结束时停止。于是这里就用到了 TestNG 的 @BeforeSuite 和 @AfterSuite 注解。
Server 启动函数:
@Parameters({"node", "appium.js", "port", "bootstrap_port", "chromedriver_port","udid"}) @BeforeSuite public void startServer(String nodePath, String appiumPath, String port,String bootstrapPort, String chromeDriverPort, String udid) { boolean needStartServer = true; if (needStartServer) { new Thread(new Runnable() { @Override public void run() { try { AppiumServerController.getInstance().startServer(nodePath, appiumPath, port, bootstrapPort, chromeDriverPort, udid); } catch (Exception e) { e.printStackTrace(); } } }).start(); try { Thread.sleep(20000); } catch (InterruptedException e) { e.printStackTrace(); } } }
开头用到了 @Parameters 注解,在这个注解中引用在 xml 文件中配置的 parameter 标签,读取的值作为startServer(…)函数的输入参数。
使用 @BeforeSuite 注解,指定此函数在整个 TestSuite 开始时执行一次。
创建一个新线程来执行启动 Server 的任务,调用的是 AppiumServerController.getInstance().startServer()
函数,并传入通过@Parameters 注解方式获取的 xml 参数值,AppiumServerController 类后面再讲。
这里一定要启动一个新线程,因为测试期间 Server 一直在后台运行,会阻塞线程,如果和测试代码放到一个线程里,那测试将无法进行下去。
执行启动 Server 的代码后,等待了 20s,给 Server 足够的时间来启动。然而这不是一个好的做法,正确的做法应该是读取 Server 进程的输入流,当出现 Server 成功启动的信息后,再执行后面的测试,这个后面再优化。
更新,使用锁机制保证用例在server启动后执行:
在启动 server的子线程开始阶段获取锁:
protected ReentrantLock serverLock = new ReentrantLock();serverLock.lock(); AppiumServerController.getInstance().startServer(serverLock,nodePath, appiumPath, port, bootstrapPort, chromeDriverPort, udid);
当检测到命令行输出服务启动成功的信息后,释放锁
while ((line = reader.readLine()) != null) { System.out.println(line); if(line.startsWith("[Appium] Appium REST http interface listener started on")) { lock.unlock(); } }
在主线程中,先延时2秒,再尝试获取锁,这样在server启动前,主线程会处于阻塞状态,server启动获取锁后,主线程才继续执行
try { Thread.sleep(2000); //确保服务器启动的线程先获得锁 } catch (InterruptedException e) { e.printStackTrace(); } serverLock.lock(); System.out.println("Server with port "+ port + " has started!!!"); serverLock.unlock();
Server 停止函数 :
@Parameters({ "port"}) @AfterSuite public void stopServer( String port) { AppiumServerController.getInstance().stopServer(port); }
同样使用 @Parameters 传入服务器的端口 port,@AfterSuite 表明 此函数只在 TestSuite 结束的最后阶段执行一次。
通过调用 AppiumServerController.getInstance().stopServer(port);
来停止端口后为 port 的AppiumServer。
2.2 连接终端
@Parameters({"port", "platformName", "platformVersion", "deviceName", "appPackage", "appActivity" , "noReset", "app"}) @BeforeTest public void setUp(String appiumPort, String platformName, String platformVersion, String deviceName, String appPackage, String appActivity, String noReset, String app) { System.out.println("[-----------Paramaters-----------] port=" + appiumPort); capabilities.setCapability("platformName", platformName); capabilities.setCapability("platformVersion", platformVersion); capabilities.setCapability("deviceName", deviceName); capabilities.setCapability("appPackage", appPackage); capabilities.setCapability("appActivity", appActivity); capabilities.setCapability("noReset", noReset); capabilities.setCapability("app", app); capabilities.setCapability("unicodeKeyboard", true); capabilities.setCapability("resetKeyboard", true); System.out.println(capabilities.toString()); try { driver = new AndroidDriver<AndroidElement>(new URL("http://127.0.0.1:" + appiumPort + "/wd/hub"), capabilities); } catch (MalformedURLException e) { e.printStackTrace(); } } @AfterTest public void tearDown() throws Exception { driver.quit(); }
setUp() 函数,利用 @Parameters 传入 能力集参数,@BeforeTest 表明在每个用例之前都会执行,函数中主要执行 AndroidDriver 的初始化,执行后终端会启动相应的 APP,准备测试。
tearDown()函数,@AfterTest 表明在每个用例之后都会执行,退出 driver,相应终端也会退出 APP。
3. AppiumServerController 类
3.1 使用单例模式
AppiumServerController 用来控制全局所有 Appium Server,需要记录所有已启动的 Server 进程,因此用单例模式,全局只存在一个实例。静态创建一个 appiumServerController 实例,再将构造函数私有化,并公开一个 getInstance()来获取这个实例。
另外用一个 HashMap 来保存 Server 的 Process ,用 port 作为唯一标识的 key。
public class AppiumServerController { //private Process mProcess; private HashMap<String, Process> processHashMap = new HashMap<>(); private String nodePath = "node"; private String appiumJsPath; private String port; private String bootstrapPort; private String chromedriver_port; private String UID; private static AppiumServerController appiumServerController = new AppiumServerController(); private AppiumServerController() { } public static AppiumServerController getInstance() { return appiumServerController; }
3.2 Server 的启动和停止
public void startServer(String nodePath, String appiumPath, String port, String bootstrapPort, String chromeDriverPort, String udid) throws Exception { Process process; String cmd = nodePath + " \"" + appiumPath + "\" " + "--session-override " + " -p " + port + " -bp " + bootstrapPort + " --chromedriver-port " + chromeDriverPort + " -U " + udid; System.out.println(cmd); process = Runtime.getRuntime().exec(cmd); processHashMap.put(port, process); System.out.println(process); InputStream inputStream = process.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String line; while ((line = reader.readLine()) != null) { System.out.println(line); } process.waitFor(); System.out.println("Stop appium server"); inputStream.close(); reader.close(); process.destroy(); } public void stopServer(Process process) { if (process != null) { System.out.println(process); process.destroy(); } } public void stopServer(String port) { Process process = processHashMap.get(port); stopServer(process); processHashMap.remove(port); }
startServer() 函数中,首先利用所有的输入参数,合成一条命令 cmd
,再用process = Runtime.getRuntime().exec(cmd);
来执行命令,并获取执行后的服务器进程 process ,接下来通过processHashMap.put(port, process);
将 process 保存起来。因为 Server 一直在后台执行,所以 process.waitFor();
及后面的语句并不会主动执行,只有在强制结束 Server 后才会执行。
stopServer() 函数,首先通过传入的 port 找到对应的 process ,调用 process.destroy();
将其停止,并移除出 HashMap。
4. 用例执行顺序
先看一用例示意:
package main.java.test;import main.java.AppiumTestCase;import org.testng.annotations.Test;public class TestAnimation extends AppiumTestCase { @Test public void testAnimation() { sleep(2000); sendWithInfo(new String[]{"Animation", ""}, 5000); sleep(5000); }}
用例 TestAnimation 类继承自前文描述的用例基类 AppiumTestCase,用例函数使用注解 @Test 即可,用例内容用简单示例,先不关注。这个用例需要放到xml 文件中:
<test name="WebView"> <classes> <class name="main.java.test.TestWebView"/> </classes> </test>
一个TestSuite下的执行顺序就是:
- @BeforeSuite ,启动 Server
- @BeforeTest, 连接终端,启动APP
- @Test,执行用例1
- @AfterTest,退出APP
- @BeforeTest, 连接终端,启动APP
- @Test,执行用例2
- @AfterTest,退出APP
- 。。。
- @AfterSuite,停止 Server
多个TestSuite,则并行得执行上述过程,达到在多个终端上并发测试的效果。
- Java + TestNG + Appium 实现单机多个Android终端并发测试
- appium单机并发测试-入门
- [Android测试] AS+Appium+Java+Win 自动化测试之十:testng多设备并行测试实例封装
- [Android测试] AS+Appium+Java+Win 自动化测试之四: 单元测试框架和TestNg
- Appium+TestNG实现Android真机自动化测试 安装配置说明
- Saucelabs+Java+TestNG+Appium+Maven+Git+Jenkins+ReportNG for Android 自动化测试
- 基于 Appium + testNG + Java + maven 适用于Android、iOS自动化测试框架
- SVN+Jenkins+Maven+Appium+TestNG+ReportNG 实战 Android 自动化测试
- SVN+Jenkins+Maven+Appium+TestNG+ReportNG 实战 Android 自动化测试
- SVN+Jenkins+Maven+Appium+TestNG+ReportNG 实战 Android 自动化测试
- SVN+Jenkins+Maven+Appium+TestNG+ReportNG 实战 Android 自动化测试
- SVN+Jenkins+Maven+Appium+TestNG+ReportNG 实战 Android 自动化测试
- Java + testng +maven + appium for IOS 自动化测试
- Appium+java实现自动化测试第一个demo
- TestNG的testng.xml实现多个测试用例的不同组合
- TestNG并发运行多个浏览器之TestNG.xml配置
- Appium TestNg Maven Android Eclipse java简单启动实例
- TestNG 八 并发测试
- Centos7+svn+jenkins+maven+nexus+SonarQube+docker持续集成安装(一)
- Centos7+svn+jenkins+maven+nexus+SonarQube+docker持续集成安装(二)
- Sort Sort Sort
- Cows POJ
- 实验一:顺序表
- Java + TestNG + Appium 实现单机多个Android终端并发测试
- MPU6050的数据获取、分析与处理
- 站在线程角度看Android Handler 机制
- 数据结构 2 算法
- Codeforces Round #436 div 2 A B C D 题解
- Mysql Mac osX 下安装及初始设置和问题解决
- 为什么设置了jackson2序列化方式hash的get方法不能反序列化对象
- vs2012设置查看内存
- 脚本化HTTP——AJax