您的位置:首页 > 移动开发 > Android开发

[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: 技术只做参考,希望朋友自身多多思考
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐