热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java+TestNG+Appium实现单机多个Android终端并发测试

前言我们知道,单台PC上用Appium连接多个Android终端进行测试时,需要同时用不同的端口号启动不同的AppiumServer,例如启动两个服务器:nodem

前言

我们知道,单台 PC 上用 Appium 连接多个 Android 终端进行测试时,需要同时用不同的端口号启动不同的 Appium Server,例如启动两个服务器:

node main.js -p 4723 -bp 4724 -chromedriver-port 9515 -U emulator1
node 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 服务器。
portbootstrap_portchromedriver_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 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(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 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,则并行得执行上述过程,达到在多个终端上并发测试的效果。


推荐阅读
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 关于我们EMQ是一家全球领先的开源物联网基础设施软件供应商,服务新产业周期的IoT&5G、边缘计算与云计算市场,交付全球领先的开源物联网消息服务器和流处理数据 ... [详细]
  • 本文详细介绍了解决全栈跨域问题的方法及步骤,包括添加权限、设置Access-Control-Allow-Origin、白名单等。通过这些操作,可以实现在不同服务器上的数据访问,并解决后台报错问题。同时,还提供了解决second页面访问数据的方法。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 如何在服务器主机上实现文件共享的方法和工具
    本文介绍了在服务器主机上实现文件共享的方法和工具,包括Linux主机和Windows主机的文件传输方式,Web运维和FTP/SFTP客户端运维两种方式,以及使用WinSCP工具将文件上传至Linux云服务器的操作方法。此外,还介绍了在迁移过程中需要安装迁移Agent并输入目的端服务器所在华为云的AK/SK,以及主机迁移服务会收集的源端服务器信息。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
author-avatar
Q_jack
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有