[Android测试] AS+Appium+Java+Win 自动化测试之十:testng多设备并行测试实例封装
2016-12-07 10:39
821 查看
一、什么是并行测试
多台设备同时执行多个用例。。。二、原理
appium启动多个服务,每个服务对应一个手机,占用不同的服务端口。利用testng的多线程实现并行。网上有些教程说grid,然后加什么json,这是以前selendriod 的并行方法了。appium是不用那么复杂的,那个json是配置信息,我们在testng文件和脚本里面已经配置好了。
还有启动appium服务端用命令是最方便的,你硬是要用gui客户端也行,启动多几个appium的gui客户端,配置好bootstrap和appium服务端口,然后启动服务就行了,我在这里就不详解了。
1. testng文件例子
<?xml version="1.0" encoding="UTF-8"?> <suite name="Suite" parallel="tests" thread-count="2"> <!-- Reportng的监听器--> <listeners> <listener class-name="org.uncommons.reportng.HTMLReporter"/> <listener class-name="org.uncommons.reportng.JUnitXMLReporter"/> </listeners> <!-- 第一个手机的测试用例 --> <test name="6533d70_Login"> <!-- appium端口号 --> <parameter name="port" value="6666"/> <!-- 手机的udid --> <parameter name="udid" value="6533d70"/> <classes> <class name="com.example.cases.Login"/> </classes> </test> <!-- 第二个手机的测试用例 --> <test name="JBORPNPZAQMBDIZH_Login"> <parameter name="port" value="6667"/> <parameter name="udid" value="JBORPNPZAQMBDIZH"/> <classes> <class name="com.example.cases.Login"/> </classes> </test> </suite>
2. 脚本接收参数
添加@Parameters({“udid”,”port”})注解接收testng的参数值,,初始化的时候添加udid和port。三、流程
获取手机设备udid判断端口是否可以用,生成开启appium服务的命令
运气开启appium服务命令
生成设备信息文件和生成testng文件
运行testng文件进行测试
不想测试了就运行StopServer停止服务
四、实现
我这里把流程都已经封装好了,运行StartServers.kava 即可自动生成对应的testng文件,然后运行这个testng即可进行测试,不想测试了就运行StopServer停止服务。(ps: driver的初始化请放在BeforeClass或者BeforeTest,如果你放在BeforeSuite的话,就会导致只有一个手机执行,因为BeforeSuite注解的方法将只运行一次,运行在所有测试前)
CmdCtrl: cmd命令的控制类,单例的方式执行cmd命令
FileCtrl: 文件的控制类,获取log,xml文件的路径
PortCtrl: 端口的控制类,判断端口是否占用,获取端口列表
ServerCtrl: 服务的控制类,获取手机udid列表,生成启动服务命令,关闭服务,获取启动的端口列表,获取对应的pid列表
StartServers: 启动服务,手机插好之后,run这个文件即可在module下自动生成testng文件
StopServers: 停止服务,测试完成之后,可以run这个文件,关闭appium服务
XmlUtils: xml工具类,保存在运行的设备的信息和testng做生成和解析获取
1. CmdCtrl.java
package com.example.utils; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; /** * cmd命令控制类 * Created by Litp on 2016/12/1. */ public class CmdCtrl { private static CmdCtrl cmdCtrl; private Runtime runtime = Runtime.getRuntime(); public static CmdCtrl getInstance(){ if(cmdCtrl == null){ cmdCtrl = new CmdCtrl(); } return cmdCtrl; } /** * 运行cmd,并且返回结果 * * @param command 要运行的命令 * @return */ public List<String> execCmd(String command) { if (!command.isEmpty()) { BufferedReader br = null; try { //执行cmd命令 Process process = runtime.exec("cmd /c " + command); br = new BufferedReader(new InputStreamReader(process.getInputStream(),"GBK")); String line = ""; List<String> content = new ArrayList<>(); while ((line = br.readLine()) != null){ if (!line.isEmpty()) { content.add(line); } } //process.destroy(); return content; } catch (Exception e) { System.out.println("execCmd执行命令错误!" + e.getMessage()); } finally { if (br != null) { try { br.close(); } catch (Exception e) { e.printStackTrace(); } } } } return null; } /** * 执行cmd命令看看有没有成功执行 * @param command 对应的命令 * @return */ public Boolean execCmdTrue(String command){ try { //执行cmd命令 Process process = runtime.exec("cmd /c " + command); //process.waitFor(); //process.destroy(); return true; } catch (Exception e) { System.out.println("execCmdTrue的cmd命令执行错误" + e.getMessage()); return false; } } }
2. FileCtrl.java
package com.example.utils; import org.apache.commons.io.FileUtils; import org.openqa.selenium.OutputType; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import javax.swing.filechooser.FileSystemView; import io.appium.java_client.android.AndroidDriver; /** * Created by Litp on 2016/12/2. */ public class FileCtrl { /** * 获取桌面路径 * @return */ private static String getDesktopPath(){ FileSystemView fsv = FileSystemView.getFileSystemView(); File com=fsv.getHomeDirectory(); //这便是读取桌面路径的方法了 return com.getAbsolutePath(); } /** * 获取当前项目路径 * @return */ public static String getProjectPath(){ return System.getProperty("user.dir")+"autotest/src/main/java/com/example/utils/"; } public static String getModulePath(){ return System.getProperty("user.dir")+"/autotest/"; } public static String getLogsPath(){ return getModulePath()+"src/main/java/com/example/logs/"; } public static String getPackageName(){ return "com.example.cases."; } /** * 删除文件 * @return */ public static boolean delFile(String filePth){ boolean flag = false; File file = new File(filePth); // 路径为文件且不为空则进行删除 if (file.isFile() && file.exists()) { if(file.delete()){ flag = true; System.out.println("删除文件成功:"+filePth); }else{ System.out.println("删除文件失败:"+filePth); } } return flag; } }
3. PortCtrl.java
package com.example.utils; import java.util.ArrayList; import java.util.List; /** * 端口控制类 * Created by Litp on 2016/12/2. */ public class PortCtrl { /** * 判断端口是否被占用 * * @param portNum 端口号 * @return */ private static Boolean isPortUsed(int portNum) { List<String> portRes = new ArrayList<>(); boolean flag = true; //是否被占用 try { // portRes = CmdCtrl.getInstance().execCmd("netstat -an|findstr " + portNum); if (portRes.size() > 0) { System.out.println("端口" + portNum + "已被占用"); } else { System.out.println("端口" + portNum + "没有被占用"); flag = false; } return flag; } catch (Exception e) { System.out.println("获取端口占用情况失败!="); } return flag; } /** * 创建可用的端口列表,是个设备就是20个端口,因为一个设备有2个端口需要开通 * * @param startPort 开始的端口 * @param devicesTotal 设备总数 * @return */ public static List<Integer> createPortList(int startPort, int devicesTotal) { List<Integer> portList = new ArrayList<>(); while (portList.size() != devicesTotal) { if (startPort > 0 && startPort < 65535) { if(!isPortUsed(startPort)){ portList.add(startPort); } startPort = startPort + 1; } } return portList; } /** * 根据设备数量来生成可用端口列表 * @param startPort 起点端口 * @return */ public static List<Integer> getPortList(int startPort){ List<String> deviceList = ServerCtrl.getUdidList(); List<Integer> portList = new ArrayList<>(); if(deviceList != null){ portList = createPortList(startPort,deviceList.size()); } return portList; } }
4. ServerCtrl.java
package com.example.utils; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * appium服务控制类 */ public class ServerCtrl { //设备udid list public static List<String> udidList; /** * 获取当前链接的手机的udid列表 * * @return */ public static List<String> getUdidList() { if (udidList == null || udidList.isEmpty()) { udidList = new ArrayList<>(); List<String> list = CmdCtrl.getInstance().execCmd("adb devices"); if (list != null && !list.isEmpty()) { for (int i = 0; i < list.size(); i++) { if (i != 0) { String[] devicesInfo = list.get(i).split("\t"); //状态为device才是正确链接了手机,如果是offline、组织 try { if (devicesInfo[1].equals("device")) { System.out.println("成功获取设备:" + devicesInfo[0].trim()); udidList.add(devicesInfo[0].trim()); } } catch (ArrayIndexOutOfBoundsException e) { //跳过两行 // * daemon not running. starting it now on port 5037 * // * daemon started successfully * i = i + 2; } } } } else { System.out.println("当前没有手机链接..."); return null; } if (udidList.isEmpty()) { System.out.println("有" + list.size() + "台手机链接,但是手机没有正确链接,请尝试重新链接手机"); } } return udidList; } /** * 创建 启动服务的命令 * * @return */ public static List<String> createServerCommand() throws Exception { //appium服务的端口号 List<Integer> appiumPortList = PortCtrl.getPortList(6666); //bootstrap的端口号 List<Integer> bsPortList = PortCtrl.getPortList(9999); //获取手机的udid列表 List<String> devicesList = getUdidList(); List<String> commandList = new ArrayList<>(); //对应log的名字,保存起来可以提供删除 List<String> logNameList = new ArrayList<>(); //生成开启服务的命令,把对应的日志保存到D盘的AppiumLogs目录下 for (int i = 0; i < devicesList.size(); i++) { String logName = devicesList.get(i) + "_" + XmlUtils.getCurrentTime() + ".log"; String command = "appium.cmd --address 127.0.0.1 -p " + appiumPortList.get(i) + " -bp " + bsPortList.get(i) + " --session-override -U " + devicesList.get(i) + ">" + FileCtrl.getLogsPath()+logName; commandList.add(command); logNameList.add(logName); } //把设备信息保存起来,启动服务之后可以自动生成testng XmlUtils.createDeviceXml(devicesList, appiumPortList,logNameList); return commandList; } /** * 根据进程pid杀死进程,用在结束测试之后,杀死那些端口 * * @param pid 要杀死的pid进程 * @return */ public static Boolean killServerByPid(String pid) { if (CmdCtrl.getInstance().execCmdTrue("taskkill -f -pid " + pid)) { System.out.println("根据pid:" + pid + "杀死进程成功"); return true; } else { System.out.println("根据pid:" + pid + "杀死进程失败"); return false; } } /** * 获取上一次开启服务端口 * * @return */ public static List<String> getStartPortList() throws Exception { List<Map<String, String>> mapList = new ArrayList<>(); mapList = XmlUtils.readDevicesXml(FileCtrl.getModulePath() + "devicesInfo.xml"); List<String> portList = new ArrayList<>(); for (Map<String, String> map : mapList) { String port = map.get(XmlUtils.APPIUMPORT); portList.add(port); } return portList; } /** * 占用服务的程序的pid * * @return */ public static List<String> getStartPidList(List<String> portList) throws Exception { List<String> pidList = new ArrayList<>(); if (!portList.isEmpty()) { for (String port : portList) { //根据端口查询对应占用程序的pid List<String> resultList = CmdCtrl.getInstance().execCmd("netstat -aon | findstr " + port); if (!resultList.isEmpty()) { for (String line : resultList) { //利用正则表达式来获取pid Pattern p = Pattern.compile(" (\\d{2,5})$"); Matcher m = p.matcher(line); if (m.find()) { String pid = m.group(m.groupCount()); //不存在就add进pid列表 if (!pidList.contains(pid)) { pidList.add(pid); } } } } } } return pidList; } }
5. StartServers.java
package com.example.utils; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 根据手机启动服务 * Created by Litp on 2016/12/2. */ public class StartServers { public static void main(String[] args) { //执行的用例 List<String> classList = new ArrayList<>(); if(args.length > 0){ //运行时候传递了参数进来 classList.addAll(Arrays.asList(args)); }else{ //手动添加 classList.add(FileCtrl.getPackageName()+ "Login"); } try { if(startServers(classList)){ System.out.println("开启服务完成"); }else{ System.out.println("开启服务失败,要执行的命令行为空"); } } catch (Exception e) { e.printStackTrace(); System.out.println("开启服务失败"+e.getMessage()); } } /** * 启动服务 * @return 返回时候执行命令成功 * @param className 用例名称 Login Index * @throws Exception 开启过程中的异常 */ public static boolean startServers(List<String> className) throws Exception{ List<String> startCommandList = ServerCtrl.createServerCommand(); boolean flag ; if(startCommandList.size() > 0){ for(String str:startCommandList){ //执行cmd命令 if(CmdCtrl.getInstance().execCmdTrue(str)){ System.out.println("开启服务成功:"+str); }else{ System.out.println("开启服务失败:"+str); } } flag = true; //创建testbg文件,0就是全部设备 XmlUtils.createTestNgXml(0,className); }else{ flag = false; } return flag; } }
6. StopServers.java
package com.example.utils; import java.util.List; /** * Created by Litp on 2016/12/2. */ public class StopServers { public static void main(String[] args){ stopServers(); } /** * 停止 服务 */ public static void stopServers(){ try { List<String> pidList = ServerCtrl.getStartPidList(ServerCtrl.getStartPortList()); for(String pid:pidList){ //傻吊进程 ServerCtrl.killServerByPid(pid); } //删除设备文件 //FileCtrl.delFile(FileCtrl.getModulePath()+"devicesInfo.xml"); } catch (Exception e) { System.out.println("停止服务时候获取运行服务对应的进程pid失败"+e.getMessage()); e.printStackTrace(); } } }
7. XmlUtils.java
package com.example.utils; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.io.OutputFormat; import org.dom4j.io.SAXReader; import org.dom4j.io.XMLWriter; import java.io.File; import java.io.FileOutputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** * xml管理,用到了dom4j库 * Created by Litp on 2016/12/2. */ public class XmlUtils { public final static String DEVICE = "device"; public final static String DEVICEID = "deviceId"; public final static String DEVICENAME = "deviceName"; public final static String APPIUMPORT = "appiumPort"; public final static String LOGNAME = "logName"; /** * 创建设备和对应服务的xml信息, * * @param devicesList 手机列表 * @param appiumPortList 端口列表 * @param logNameList 保存的log的名字 */ public static void createDeviceXml(List<String> devicesList, List<Integer> appiumPortList, List<String> logNameList) throws Exception { Document document = DocumentHelper.createDocument(); //创建根元素:<Device></Device> Element root = DocumentHelper.createElement(DEVICE); document.setRootElement(root); //根元素Device添加一个属性appiumStartList:<Device appiumStartList=""></Device> root.addAttribute("name", "devicesList"); if (!devicesList.isEmpty()) { for (int i = 0; i < devicesList.size(); i++) { //在根元素下创建对应元素deviceId: Element deviceId = root.addElement(DEVICEID); //为 deviceId.addAttribute("id", i + ""); //在deviceId元F素下创建对应元素deviceName: Element deviceName = deviceId.addElement(DEVICENAME); //在deviceId元素下创建对应元素appiumPort: Element appiumPort = deviceId.addElement(APPIUMPORT); //在deviceId元素下创建对应元素appiumPort: Element logName = deviceId.addElement(LOGNAME); //设置deviceName的文本 <deviceName>要设置的文本</deviceName> deviceName.setText(devicesList.get(i)); //设置appiumPort的文本 appiumPort.setText(appiumPortList.get(i) + ""); //设置logName的文本 logName.setText(logNameList.get(i)); } //生成testng.xml OutputFormat format = new OutputFormat(" ", true); XMLWriter xmlWriter = null; try { xmlWriter = new XMLWriter(new FileOutputStream(FileCtrl.getModulePath() + "devicesInfo.xml"), format); xmlWriter.write(document); System.out.println("生成设备信息文件"); } catch (Exception e) { System.out.println("生成设备信息文件失败"); } } } /** * 创建Testng xml文件 到module根目录 * * @param threadCount 线程数,0 是根据手机数量来生成 * @param className 测试类的类名 */ public static void createTestNgXml(int threadCount, String className) throws Exception { Document document = DocumentHelper.createDocument(); Element root = DocumentHelper.createElement("suite"); document.setRootElement(root); root.addAttribute("name", "Suite"); //设备信息的list List<Map<String, String>> devicesInfo = readDevicesXml(FileCtrl.getModulePath() + "devicesInfo.xml"); //线程数为0 或者线程数大于设备数就添加全部手机 if (threadCount == 0 || threadCount > devicesInfo.size()) { root.addAttribute("parallel", "tests"); root.addAttribute("thread-count", devicesInfo.size() + ""); } else { root.addAttribute("thread-count", "1"); } //创建listeners 监听器元素 Element listeners = root.addElement("listeners"); //创建listenerHtml元素 Element listenerHtml = listeners.addElement("listener"); Element listenerXML = listeners.addElement("listener"); //添加报告监听器 listenerHtml.addAttribute("class-name", "org.uncommons.reportng.HTMLReporter"); listenerXML.addAttribute("class-name", "org.uncommons.reportng.JUnitXMLReporter"); //循环创建对应的test for (int i = 0; i < ((threadCount == 0 || threadCount > devicesInfo.size()) ? devicesInfo.size() : threadCount); i++) { //创建test元素 Element test = root.addElement("test"); //每个test的名字要不一样,这里以设备udid_类名进行区分 test.addAttribute("name", devicesInfo.get(i).get(DEVICENAME) + "_" + className.get(0).substring(className.get(0).lastIndexOf(".") + 1)); //在test下创建port端口parameter元素 Element port = test.addElement("parameter"); port.addAttribute("name", "port"); port.addAttribute("value", devicesInfo.get(i).get(APPIUMPORT)); //在test下创建udid端口parameter元素 Element udid = test.addElement("parameter"); udid.addAttribute("name", "udid"); udid.addAttribute("value", devicesInfo.get(i).get(DEVICENAME)); //创建classes 执行用例元素 Element classes = test.addElement("classes"); //添加要执行的用例 for(String cls:className){ //创建class元素 Element classElement = classes.addElement("class"); classElement.addAttribute("name", cls); } } //生成testng.xml OutputFormat format = new OutputFormat(" ", true); XMLWriter xmlWriter = null; try { xmlWriter = new XMLWriter(new FileOutputStream(FileCtrl.getModulePath() + "testng_" + getCurrentTime() + ".xml"), format); xmlWriter.write(document); System.out.println("生成testng文件"); } catch (Exception e) { System.out.println("生成testng文件失败"); } } /** * 获取当前的时间 年月日时分秒 * * @return */ public static String getCurrentTime() { SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMddHHmmss"); Date now = new Date(); return dateFormat.format(now); } public static Document getDevicesDocument(String fileName) throws DocumentException { //将src下面的xml转换为输入流 //InputStream inputStream = new FileInputStream(new File(fileName)); //也可以根据类的编译文件相对路径去找xml //InputStream inputStream = this.getClass().getResourceAsStream("/module01.xml"); //创建SAXReader读取器,专门用于读取xml SAXReader saxReader = new SAXReader(); //根据saxReader的read重写方法可知,既可以通过inputStream输入流来读取,也可以通过file对象来读取 //Document document = saxReader.read(inputStream); //fileName必须指定文件的绝对路径 return saxReader.read(new File(fileName)); } /** * 解析devicesInfo.xml 为 * * @param fileName 设备信息xml路径,绝对路径 * @return */ public static List<Map<String, String>> readDevicesXml(String fileName) throws Exception { Document document = getDevicesDocument(fileName); //根节点 Element element = document.getRootElement(); //每个设备的list List<Element> deviceIDList = element.elements(DEVICEID); List<Map<String, String>> devices = new ArrayList<>(); if (deviceIDList != null && !deviceIDList.isEmpty()) { //每个设备的信息 for (Element deviceID : deviceIDList) { Map<String, String> map = new HashMap<>(); map.put(DEVICENAME, deviceID.element(DEVICENAME).getText()); map.put(APPIUMPORT, deviceID.element(APPIUMPORT).getText()); devices.add(map); } } return devices; } }
ps: 技术只做参考,希望朋友自身多多思考
相关文章推荐
- [Android测试] AS+Appium+Java+Win 自动化测试之八:使用PageObject模式和重封装
- [Android测试] AS+Appium+Java+Win 自动化测试之四: 单元测试框架和TestNg
- [Android测试] AS+Appium+Java+Win 自动化测试之六 Appium的Java测试脚本封装
- [Android测试] AS+Appium+Java+Win 自动化测试之六 Appium的Java测试脚本封装
- [Android测试] AS+Appium+Java+Win 自动化测试之九:PO模式的实例与ReportNg测试报告
- [Android测试] AS+Appium+Java+Win 自动化测试之七: 写脚本测试自己的app
- [Android测试] AS+Appium+Java+Win 自动化测试之五:脚本重点技术
- AS+Appium+Java+Win 自动化测试之八:使用PageObject模式和重封装
- [Android测试] Android Studio+Appium+Java+windows 自动化测试之一: 自动化测试理解
- Saucelabs+Java+TestNG+Appium+Maven+Git+Jenkins+ReportNG for Android 自动化测试
- [Android测试] Android Studio+Appium+Java+Windows 自动化测试之二:Appium环境安装搭建
- APP接口自动化测试JAVA+TestNG(三)之HTTP接口测试实例
- APP接口自动化测试JAVA+TestNG之HTTP接口测试实例
- Appium Android ——利用 TestNG 并行执行用例
- 零成本实现接口自动化测试 – Java+TestNG 测试Restful service
- SVN+Jenkins+Maven+Appium+TestNG+ReportNG 实战 Android 自动化测试
- appium 通过testng 实现在不同的测试机上并行执行测试用例
- [Android测试] Appium的Java-client库api
- APP接口自动化测试JAVA+TestNG(二)之TestNG简介与基础实例
- appium android——利用testng和maven并行执行用例