[Appium][Python]pytest-xdistを使ってparallel-testsを実現する

Selenium/Appium Advent Calendar 2018、4日目の記事です。

1 日目の 2018 年の Selenium と Appium について、なるべく他の人とかぶらないことに言及するテスト でも言及いただいたのですが、ちょうどこの 12 月より HeadSpin にて活動をはじめました。現状はまだ日本に滞在しています。


今、私は Appium project をメンテしています。その中で感じたここ最近の 普通 になったなということに並列実行(parallel execution)があります。

Appium の公式ドキュメントでも parallel-tests にある通り、その実行方法に関して言及しています。人によっては Selenium Grid を介していたりもします。その中で大事なものは、Appium の各種 Driver と通信できるように、ポートの設定とテスト端末の特定を適切に行うことです。

Appiumの基本構成としては、各種clientライブラリがAppiumサーバと通信します。その先は、Appiumサーバが各種Driver(例えばappium-xcuitest-driver)を経由して端末にインストールされたサーバ(例えばWebDriverAgent)と通信してOS提供のAPIを利用して操作を模倣します。そのため、それぞれの間で行われる通信が正しく行われる必要があります。

https://github.com/appium/ruby_lib_core#run-tests では、 parallel_tests を利用した parallelisation を例の 1 つとして実行可能にしています。 https://github.com/appium/ruby_lib#run-tests-in-parallel ではいくつかの parallelisation を載せています。例えば、Thread を使ったものや Process を使ったものを。

ここでは、Ruby ではなく、Python を使った例を載せておこうと思います。
Appium の parallelisation には以下の2通りの実現方法があります。

  • 1 つの Appium インスタンスに複数のテスト端末を同時接続する
  • 1 つの Appium インスタンスと 1 つのテスト端末のペアを複数起動する

実行資源の制限から前者を選ぶ必要があるかもしれません。後者の利点は各々の Appium インスタンスのプロセスが別なので、何らかの Appium サーバ側の処理遅延の影響などを互いに与えあわないことでしょうか。歴史的な経緯を除けば、まずは前者、並列度をあげていきたい時には後者にしていくという方法が楽で良いのではないでしょうか。もしくは、その実行環境をHeadSpinや他DeviceFarmなどを利用する、というのもある程度の運用期間を考えると良い選択肢だと思います。

以下では前者の話をします。

Run tests in parallel by pytest-xdist in Python

実は、この実行方法もすでに公式リポジトリに含めています。https://github.com/appium/python-client#in-parallel-for-ios

以下ではコード例を置いて、コメントとして各々が何をしているのかを残します。なお、ここでは pytest-xdist を利用しています。基本的に、他の言語においても同様の対応をするとAppiumとしては parallelisation 可能です。

# iOSの場合にparallelisationで大事なのは、`deviceName` と `wdaLocalPort` を適切な値にすることです。
def get_desired_capabilities(app):
    desired_caps = {
        'deviceName': iphone_device_name(),
        'platformName': 'iOS',
        'platformVersion': '12.1',
        'app': app,
        'automationName': 'XCUITest',
        'wdaLocalPort': wda_port(),
    }

    return desired_caps

# pytest-xdist では、どのworkerがテストケースを実行しているのかを環境変数 `PYTEST_XDIST_WORKER` によって取得することができます。
# そのため、そのworkerに見合った `deviceName` と `wdaLocalPort` を設定してあげる必要があります。
class PytestXdistWorker(object):
    NUMBER = os.getenv('PYTEST_XDIST_WORKER')
    COUNT = os.getenv('PYTEST_XDIST_WORKER_COUNT')  # Returns 2 if `-n 2` is passed

    @staticmethod
    def gw(number):
        if PytestXdistWorker.COUNT is None:
            return '0'

        if number >= PytestXdistWorker.COUNT:
            return 'gw0'

        return 'gw{}'.format(number)

# ここでは、 `wdaLocalPort` の割り振りを決めます。愚直に固定値で返しています。
def wda_port():
    if PytestXdistWorker.NUMBER == PytestXdistWorker.gw(1):
        return 8101

    return 8100

# ここでは、 `deviceName` の割り振りを決めます。`udid`をベースに利用しているかたは、`udid`を割り振る形にするのも手ですね。ここではSimulatorを想定しているので `deviceName` をベースにしています。
def iphone_device_name():
    if PytestXdistWorker.NUMBER == PytestXdistWorker.gw(0):
        return 'iPhone 8 - 8100'
    elif PytestXdistWorker.NUMBER == PytestXdistWorker.gw(1):
        return 'iPhone 8 - 8101'

    return 'iPhone 8'

# テストコード
def test():
    desired_caps = desired_capabilities.get_desired_capabilities('path/to/app')
    driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
    # 以降、driverを使ったテストコードを書く

基本的にはこの capability の設定箇所だけです。このcapabilityの各種portなどを設定すると、テストケースの実装言語の周辺ライブラリを使ったりしながらparallelisation可能になります。

ここが終われば、あとは 1 つAppium サーバを起動し、以下のように実行してあげるとOkです。

$ pip install pytest pytest-xdist
$ py.test -n 2 test/target.py

まとめ

parallel-tests に書かれているうちの、 Parallel iOS Tests の項目を中心にiOS(+ Python)における例を載せました。Java だと AppiumTestDistribution なんかが既存ツールとしては広く使われている感じがしますね。

少し前、Genymotion もこの記事 のようにPython を使った例を公開していました。このように実行系だけを外にだして、メンテナンスコストのかかるところを手放すという選択肢も現実的ですね。私の働く HeadSpinをはじめ、 https://github.com/appium/appium-desktop/blob/master/app/shared/cloud-providers/config.js にあるサービスは基本的にAppiumをサポートしている模様です。

こちら、以前書いた https://kazucocoa.wordpress.com/2018/09/01/appiumpythonrun-tests-in-parallel-with-pytest/ に日本語で肉付けした記事になります。

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.