一、什么是并行测试

多台设备同时执行多个用例。。。

二、原理

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。

TestNG执行顺序 单线程_Android自动化

TestNG执行顺序 单线程_自动化测试_02

三、流程

  1. 获取手机设备udid
  2. 判断端口是否可以用,生成开启appium服务的命令
  3. 运行开启appium服务命令
  4. 生成设备信息文件和生成testng文件
  5. 运行testng文件进行测试
  6. 不想测试了就运行StopServer停止服务

TestNG执行顺序 单线程_Android自动化_03

四、实现

我这里把流程都已经封装好了,运行StartServers.java 即可自动生成对应的testng文件,然后运行这个testng即可进行测试,不想测试了就运行StopServer停止服务。

driver的初始化请放在BeforeClass或者BeforeTest,如果你放在BeforeSuite的话,就会导致只有一个手机执行,因为BeforeSuite注解的方法将只运行一次,运行在所有测试前

TestNG执行顺序 单线程_自动化测试_04

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;
    }


}

技术只做参考,希望朋友自身多多思考