Java+TestNG+Appium単機複数Android端末同時テストを実現

32018 ワード

前言
1台のPCで複数のAndroid端末をAppiumで接続してテストする場合、2つのサーバを起動するなど、異なるポート番号で異なるAppiumサーバを同時に起動する必要があることを知っています.
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テストコードから分離したテスト例を異なる端末に配布する必要があることを意味する.また、サーバの起動とシャットダウンもコードで自動的に実行できることが望ましい.
以前はJunit 4ユニットのテストフレームワークを使っていましたが、機能が強くなく、上記の要求を実現するのが難しいと感じました.その後、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サーバの起動を実行します.port、bootstrap_port、chromedriver_port:Appiumサーバのポート.udid:端末名は、adb devicesで調べることができます.残りのパラメータはDesiredCapabilitiesに必要なパラメータです.
第1の端末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
ode_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>

第2の端末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
ode_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>

2つのAndroid端末(シミュレータまたは実機)を接続し、RunSuiteクラスを実行します.
同時テストを開始し、2つの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をロードするには2つの方法があります.1つは、それぞれのTestSuiteのxmlファイルを個別に追加することです.
testFieldList.add("testng1.xml");
testFieldList.add("testng2.xml");

あるいは、まず要約xmlファイルを作成し、それぞれのTestSuiteを入れて、要約したファイルを1回ロードすればいいです.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");

またスレッドプールサイズの設定に注意してください.設定しないと1つのスレッドしか実行されません.ここにはTestSuiteが2つあります.2に設定できます.
testng.setSuiteThreadPoolSize(2);

2.AppiumTestCaseクラス
AppiumTestCaseは、Appiumサーバの起動と停止、およびAppiumDriverの接続と終了を含むすべてのTestCaseのベースクラスとして使用されます.
2.1 Appiumサーバーの起動と停止
1つのTestSuiteが1つの端末に対応し、1つの端末が1つのサーバに対応するため、サーバは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全体の開始時に1回実行することを指定します.
新しいスレッドを作成してサーバを起動するタスクを実行します.AppiumServerController.getInstance().startServer()関数を呼び出し、@Parameters注記で取得したxmlパラメータ値を入力します.AppiumServerControllerクラスは後述します.
ここでは、テスト中にサーバがバックグラウンドで実行され、スレッドがブロックされるため、新しいスレッドを起動する必要があります.テストコードとスレッドに入れると、テストは続行できません.
サーバーを起動するコードを実行した後、20 s待って、サーバーに十分な時間を与えて起動します.しかし、これは良い方法ではありません.正しい方法は、サーバプロセスの入力ストリームを読み取ることです.サーバが正常に起動した情報が表示されたら、後でテストを実行し、その後最適化します.
更新、ロックメカニズムを使用して使用例が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();
    }

setuUp()関数は,@Parametersを用いてコンピテンシーセットパラメータを入力し,@BeforeTestは各用例の前に実行されることを示し,関数では主にAndroid Driverの初期化を実行し,実行後端末は対応するAPPを起動し,テストの準備を行う.
tearDown()関数,@AfterTestは各使用例の後に実行され、driverを終了し、対応する端末もAPPを終了することを示しています.
3.AppiumServer Controlクラス
3.1単例モードの使用
AppiumServerControllerは、グローバルのすべてのAppiumサーバを制御するために使用されます.起動したすべてのサーバプロセスを記録する必要があります.そのため、単一のモードでは、グローバルには1つのインスタンスしか存在しません.静的にappiumServerControllerインスタンスを作成し、コンストラクション関数をプライベート化し、getInstance()を公開してインスタンスを取得します.サーバのProcessを1つのHashMapで保存し、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()関数では、まずすべての入力パラメータを用いて、1つのコマンドcmdを合成し、process = Runtime.getRuntime().exec(cmd);でコマンドを実行し、実行後のサーバプロセスprocessを取得し、次にprocessHashMap.put(port, process);でprocessを保存する.サーバはバックグラウンドで実行されているため、process.waitFor();以降の文はアクティブに実行されず、強制終了後にのみ実行されます.
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クラスは前述の用例ベースクラスApiumTestCaseから継承され、用例関数は注釈@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は、上記の手順を並行して実行し、複数の端末で同時テストの効果を達成します.