一、什么是并行测试
多台设备同时执行多个用例。。。
二、原理
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.java 即可自动生成对应的testng文件,然后运行这个testng即可进行测试,不想测试了就运行StopServer停止服务。
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;
}
}
技术只做参考,希望朋友自身多多思考