Java + TestNG + Appium for Concurrent Testing of Multiple Android Terminals on a Single Machine

Keywords: xml Java Android emulator

Preface

We know that when testing multiple Android terminals with Appium on a single PC, we need to start different Appium Server s with different port numbers at the same time, such as two servers:

node main.js -p 4723 -bp 4724 -chromedriver-port 9515 -U emulator1
node main.js -p 4725 -bp 4726 -chromedriver-port 9516 -U emulator2

The Appium Driver of the test code is then connected to the corresponding port, and the test can be executed concurrently.

This means that there are a lot of environment data to configure, including various port numbers, UDID s of terminals, and so on. At the same time, test cases need to be distributed for different terminals, which is best separated from Java test code. In addition, the startup and shutdown of the server can also be automatically executed by code.

Before I used Junit4 unit testing framework, I felt that it was not powerful enough to meet the above requirements. Later I learned about the next TestNG framework, which has its own xml mode, parametric testing, concurrent execution and other functions, just can be used to achieve the functions I envisaged.

Full code address: https://github.com/zhongchenyu/AppiumTest Now let's introduce some ideas.

Effect display

First of all, we can see the effect of the final implementation. We can complete all the testing process from the start of the server by configuring the xml file of the test suite, taking the environment information as a parameter, specifying the use cases to be executed, and executing the RunSuite class.

1. Configure the test suite xml file, each xml corresponds to a terminal:

The environment parameters are configured by the parameter tag, which includes:
The path of node: node.exe, if the system environment variable is configured, just fill in the node.
Appium.js: The path of appium.js. The new version of Appium should be the path of main.js. Cooperate with node parameters to execute startup of Appium server.
Port, bootstrap_port, chromedriver_port: The port of the Appium server.
udid: Terminal name, which can be found through adb devices.
The remaining parameters are those required by Desired Capabilities.

The first terminal 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>

The second terminal 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>

Connect two Android terminals (simulator or real machine) and run the RunSuite class:

It can start concurrent testing, automatically start two Appium servers, automatically execute the test suite, and then stop the server:

Realization principle

1. RunSuite class

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<String> 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());
    }
}

This class is relatively simple. It mainly loads the xml file of TestSuite, and then executes several test suites concurrently. All the libraries that TestNG brings are used.
There are two ways to load xml. One is to add the XML files of each TestSuite separately:

testFieldList.add("testng1.xml");
testFieldList.add("testng2.xml");

Or create a summary xml file, put each TestSuite into it, and load the summary file once.
testng_main.xml :

<suite name="Main suite">
    <suite-files>
        <suite-file path="testng1.xml"/>
        <suite-file path="testng2.xml"/>
    </suite-files>
</suite>

In RunSuite.java:

testFieldList.add("testng_main.xml");

In addition, pay attention to setting the size of the thread pool. If there is only one thread, it will not be executed concurrently. Here are two TestSuite s, which can be set to 2:

testng.setSuiteThreadPoolSize(2);

2. AppiumTestCase class

AppiumTestCase will be the base class of all TestCase, which includes the start and stop of Appium server, and the connection and exit of AppiumDriver.

2.1 Start and stop of Appium Server

Because a TestSuite corresponds to a terminal and a terminal corresponds to a Server, the Server only needs to start at the beginning of each TestSuite and stop at the end of the TestSuite. So here's TestNG's @BeforeSuite and @AfterSuite annotations.

Server starter function:

@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();
            }
        }
    }

The @Parameters annotation is used at the beginning, which refers to the parameter tag configured in the xml file and reads the value as startServer(... ) Input parameter of function.

Use the @BeforeSuite annotation to specify that this function is executed once at the beginning of the entire TestSuite.

Create a new thread to perform the task of startup Server, call the AppiumServerController.getInstance().startServer() function, and pass in the xml parameter values obtained through the @Parameters annotation, which will be discussed later in the AppiumServerController class.

It's important to start a new thread here, because Server runs in the background all the time during the test, blocking the thread, and if you put the test code in one thread, the test will not be able to proceed.

After executing the code to start the Server, wait 20 seconds to give the Server enough time to start. However, this is not a good practice, the correct approach should be to read the input stream of the Server process, when there is information about the successful start of the Server, then execute the following tests, and then optimize this later.

Server stop function:

@Parameters({ "port"})
    @AfterSuite
    public void stopServer( String port) {
        AppiumServerController.getInstance().stopServer(port);
    }

Similarly, @Parameters is used to pass in to the server's port, @AfterSuite indicates that this function is executed only once at the end of the TestSuite phase.

By calling AppiumServerController.getInstance().stopServer(port); stop the AppiumServer as port.

2.2 Connection Terminal

@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<AndroidElement>(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() function, using @Parameters to pass in capability set parameters, @BeforeTest indicates that it will be executed before each use case. The function mainly performs the initialization of Android Driver. After execution, the terminal will start the corresponding APP to prepare the test.

tearDown() function, @AfterTest indicates that after each use case, it executes, exits the driver, and the corresponding terminal exits the APP.

3. AppiumServerController class

3.1 Use the singleton model

Appium Server Controller is used to control all Appium Servers globally. It is necessary to record all started Server processes, so in singleton mode, only one instance exists globally. Statically create an instance of appiumServerController, privatize the constructor, and expose a getInstance() to get the instance.
In addition, a HashMap is used to save Server's Process, and port is used as the key of unique identification.

public class AppiumServerController {

    //private Process mProcess;
    private HashMap<String, Process> 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 Start and Stop of 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);
    }

In the startServer() function, a command CMD is synthesized with all input parameters, and then the command is executed with process = Runtime.getRuntime().exec(cmd); the server process after execution is obtained, and the process is saved through processHashMap.put(port, process). Because the Server has been executing in the background, process.waitFor(); and the following statements will not be executed actively, only after the forced end of the Server will be executed.

The stopServer() function first finds the corresponding process through the incoming port and calls process.destroy(); stops it and removes HashMap.

4. Use Case Execution Order

Let's first look at the use case hints:

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

The use case TestAnimation class inherits from the use case base class AppiumTestCase described earlier. The use case function uses the annotation @Test, and the use case content uses simple examples, without paying attention to it at first. This use case needs to be placed in an xml file:

    <test name="WebView">
        <classes>
            <class name="main.java.test.TestWebView"/>
        </classes>
    </test>

The order of execution under a Test Suite is:

  • @ BeforeSuite, start Server
  • @ BeforeTest, Connect Terminal, Start APP
  • @ Test, execute use case 1
  • @ AfterTest, quit APP
  • @ BeforeTest, Connect Terminal, Start APP
  • @ Test, Execution Case 2
  • @ AfterTest, quit APP
  • . . .
  • @ AfterSuite, stop Server

Several TestSuite s perform the above process in parallel to achieve the effect of concurrent testing on multiple terminals.

Posted by pontiac007 on Tue, 21 May 2019 15:19:15 -0700