Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

Airtest で CLI, IDE や AWS Device Farm, Local など様々なテスト実行手段、環境があったとしても楽に自動テストを実行する方法

この記事は Akatsuki Games Advent Calendar 2022 5日目の記事です。

前回はエンジニアリングオフィス 島村さんの チームの中心に置くHRT でした。HRT(謙虚、尊敬、信頼)はチームの生産性に繋がるので私も常に意識しています。TeamGeek、とても好きな本です。 hackerslab.aktsk.jp

はじめに

QAエンジニアの山﨑@tomo_tk11です。

いきなりですが、テスト自動化、やりたいですよね。でもやるからにはスマートにやりたいですよね。ただ、テスト自動化のシステムを運用フェーズまで想定して開発すると結構複雑な実行環境になってしまいます。 複雑さをそのままにして、各QAエンジニアがそれぞれの環境で開発を行うと

「あれ?自分の環境でテストが失敗するようになった」
「いやいや、僕のPCだとちゃんと成功しますよ」
「いつの間にかJenkinsでテスト通らなくなってまーす」

といったように、全環境で動作保証ができていない状態が続いてしまいます。そして、この状態は開発効率を極端に落とします。なんとかしたいですが、実行環境を整理しないままやろうとすると全環境動作保証の手間が異常にかかってしまいます。

それを解決するためには、テスト実行環境を整理した上で一気通貫した作りにする必要があります。可能な限りコードは共通化し、それぞれの環境での環境変数やテストパラメータを適切なタイミングで設定し、管理しなければいけません。

特に意識したいのは、どの環境においても「最終的に同一のテストスクリプトを実行する」ことです。どこで/どの媒体で/どういう方法でテストを実行するかは極論なんでも良くて、どの環境においても同じテストが自動で実行できること、の方が自動テストにおいては重要です。

この記事では Airtest というテストフレームワークを用いた自動テスト実行に関する情報を提供します。特に、以下の状況でできるだけシンプルに自動テストを実行するための方法をまとめます。

  • Airtestを実行する環境が複数ある(Local、AWS Device Farm)
  • テスト実行方法が複数ある(CLIによる実行、IDEによる実行)
  • テスト実行を命令する環境が複数ある(Local, Jenkins)

Airtestを取り巻く環境を広く説明するつもりなので、端末はローカルPCに接続している実機しか利用しない、テスト実行はCLIからしか行わない、という方にも参考になる情報があるかと思います。 文章量がかなり多くなってしまったので先に謝っておきます。

この記事のターゲット

  • Airtest をある程度理解している方
  • 手元のPCで Airtest の自動テストを実行することができるが、それをどう運用するか悩んでいる方
  • 自動テストを複数人でどのように開発していくか悩んでいる方

上記以外の方は 「ややこしい状況をなんかシンプルにしようと頑張ってんなー。」 くらいで読んでいただけると嬉しいです。

TL;DR

Airtest の IDE, CLI 実行 や AWS Device Farm での実行を行いつつも、全環境で「同一の自動テストスクリプトを実行する」ための環境一覧。自動テストスクリプトを記述したときに動作保証する環境はこの3つが良いと思います。この記事では、この3つの環境を作るまでの過程を解説しています。

実行命令環境 実行環境 デバイスホストマシンOS デバイス 実行方法 ファイル拡張子 Python Runtime ランチャー
Local Local Windows/Mac Unity Editor/Android/iOS Airtest IDE .air pipenv(3.9.14) custom_launcher.py
Local Local Windows/Mac/Linux Unity Editor/Android/iOS invoke → pytest .py pipenv(3.9.14) test_main.py
Local/Jenkins AWS Device Farm Linux Android/iOS invoke → AWS CLI → pytest .py セルフビルド(3.9.14) test_main.py

目次

Airtest について

Airtest はゲームにフォーカスした自動テスト用のフレームワークでNetEase社から提供されています。IDE が提供されており、テストコード(Python)を気軽に記述することができます。CLI でテスト実行も可能なので、CI環境で自動テストを実行させやすいという利点もあります。自動テストのためのアプリケーション内の操作は画像認識を駆使して自動化します。今回は触れませんが、 Poco というアプリ内のメタ情報を Airtest で利用可能にするための自動テスト用フレームワークを用いると自動テストの安定性は向上すると思います。

Airtest は自動テストフレームワークとして有名で、解説記事もググればたくさん出てくるのでここでは詳細を割愛します。テスト自動化は、Airtest IDE を用いてテストスクリプトを記述し、手元のAndroid端末で実行するだけであればわりとサクッとできます。しかし、テストスクリプトをチームで開発したり、誰でも実行できるようにしたり、テストの共通処理をモジュール化したりするとだんだんややこしくなっていきます。

Airtest version について

本記事で使用している Airtest version を記載しておきます。

Airtest IDE (Incl. Python Runtime)

Platform S/W Version ReleaseDate Python
Airtest IDE 1.2.14 2022-05-20 3.6.6

Airtest CLI (incl. ADB)

Platform S/W Version Release Date adb
Airtest cli 1.2.7 2022-09-29 1.0.40

テスト実行する環境を列挙し、厳選する

前提

基本的には mac OS の環境での開発を想定しています。細かい部分は異なりますが、他のOS環境でも同じような実行環境は実現できると思います。

  • OS: mac OS Monterey version 12.6.1
  • チップ: Apple M1 Pro

テストスクリプト(.air プロジェクト)

Airtest IDE を用いて作成するテストスクリプトは拡張子が .air になります。実際は、 .air という suffix がついたディレクトリに .py が格納されているだけです。 どんな形であれ、テストスクリプトは実行環境や実行方法とは切り離されていることが望ましいです。

なので、目標として「環境や方法に左右されない共通のテストスクリプトを記述し、それを全環境で実行できるようにすること」を目指します。

テストスクリプト実行方法

テストスクリプトを実行する方法は大きく分けると2種類あります。

  • Airtest IDE による実行
  • CLI による実行
    • Airtest CLI による実行
    • Python での実行

Airtest IDE では、テストスクリプトを実行するための Python Runtime をIDE付属のもの(3.6.6)ではなく、ユーザ指定のものに変更することができます。更に、テストケースを呼び出すランチャーも標準のものではなく、ユーザ指定のカスタムランチャーに変更することができます。

ただし、 .py の実行だとカスタムランチャーを経由して実行されません。ここが .air.py の拡張子の大きな違いになります。

「実行方法 x ファイル拡張子 x Python Runtime x ランチャー」 を考慮すると以下の表のようになります。

実行方法 ファイル拡張子 Python Runtime ランチャー
Airtest IDE .air IDE付属(3.6.6) 標準
Airtest IDE .air IDE付属(3.6.6) カスタム
Airtest IDE .py IDE付属(3.6.6) -
Airtest IDE .air ユーザ指定 標準
Airtest IDE .air ユーザ指定 カスタム
Airtest IDE .py ユーザ指定 -
Airtest CLI .air ユーザ指定 標準
Python .py ユーザ指定 -

.air のカスタムランチャーについて

通常は airtest.cli.runner の run_script がテストケースを呼び出しますが、 .air の IDE 実行時のみカスタムランチャーファイルを指定することができます。 最終的に run_script を呼び出すことに変わりはありませんが、テスト前後処理などがカスタマイズできるようになります。単純に1つのテストスクリプトを実行するだけの場合はカスタマイズしなくても良いと思いますが、テストスクリプトが増えてくると共通の処理をランチャーファイルに記述したくなってきます。後述しますが、 .py を実行する場合はカスタムランチャーを使用できないので別のファイルを用意する必要があります。

テストスクリプト実行環境

テストスクリプトを実行するためのデバイスは、ローカルPCに接続された物理デバイス、もしくはエミュレータを用いるのが一般的かと思います。しかしながら、物理デバイスというものは管理が大変なので運用フェーズではしばしば頭を悩ませる原因にもなります。自動テストを誰でも、いつでも、どこからでも、並列実行できる環境を作ろうとすると、たくさんのデバイスを管理しなければなりません。デバイスのネットワークを構築できたとしても、電源に繋ぎっぱなしになるのでバッテリーが膨らむ、ということも聞いたりします。なので、デバイス管理から解放されるべくクラウドデバイスの活用を検討します。

ここでは、 AWS Device Farm を用います。Device Farm はAWS上に物理的に配置された端末(AndroidやiOSなど)でテスト実行を可能にするテストサービスです。Airtest はテストフレームワークとしてサポートされていませんが、 Appium Python として認識させることで利用可能になります。動作させるためにはいくつかの課題がありますが、その辺りは以下の記事が参考になります。合わせて、 AWS Device Farm と Airtest でモバイルゲーム自動テスト も参考にしてください。 hackerslab.aktsk.jp

先程の表に実行環境を加えます。Device Farm ではホストマシンからデバイスに対しての命令セットとして testspec.yaml を記述し、実行したいコマンドをyaml形式で列挙します。もちろん IDE での実行はできないので、 Device Farm 上での実行方法は Python を選択します(試してませんが、 Airtest CLI でも行ける気はします)。Device Farm は AWS CLI を用いて実行します。

実行環境 実行方法 ファイル拡張子 Python Runtime ランチャー
Local Airtest IDE .air IDE付属(3.6.6) 標準
Local Airtest IDE .air IDE付属(3.6.6) カスタム
Local Airtest IDE .py IDE付属(3.6.6) -
Local Airtest IDE .air ユーザ指定 標準
Local Airtest IDE .air ユーザ指定 カスタム
Local Airtest IDE .py ユーザ指定 -
Local Airtest CLI .air ユーザ指定 標準
Local Python .py ユーザ指定 -
AWS Device Farm AWS CLI → Python .py Device Farm付属(2.7.9/3.7.4) -

AWS Device Farm の Python 実行環境について

Device Farm のホストマシンである Linux 環境について軽く触れます。

# システム、ユーザー情報 (as of 2022-08-28)
OS: Ubuntu 14.04.5 LTS (14.04 Trusty)
Architecture: x86_64
User: device-farm
User Shell: /bin/bash
Python: 2.7.9, 3.7.4
OpenSSL: 1.0.2o

# ストレージ
[DeviceFarm] df -h
Filesystem      Size  Used Avail Use% Mounted on
udev            3.7G  4.0K  3.7G   1% /dev
tmpfs           748M  332K  748M   1% /run
/dev/xvda1       30G   18G   11G  61% /
none            4.0K     0  4.0K   0% /sys/fs/cgroup
none            5.0M     0  5.0M   0% /run/lock
none            3.7G     0  3.7G   0% /run/shm
none            100M     0  100M   0% /run/user

Pythonのバージョンは testspec.yaml で選択可能ですが、 2.7.9 もしくは 3.7.4 しか選択することができません。3.7系のEOL27 Jun 2023 なので、結構ギリギリです。

Device Farm でテストを行うために必要なPythonパッケージ(Airtestも含まれる)は requirement.txt によってホストマシン上にインストールされます。パッケージの特定バージョンでセキュリティアラートが出た場合にはアップデートしつつ依存関係を解決しなければいけません。しかし、Pythonバージョンの制約があるとそのせいアップデートができない可能性もあります。

ただ、Device Farm ホストマシン上で Python ランタイムをインストール(apt)したり、ビルドするというオプションはランタイム上のユーザー(device-farm)権限の制約上、選択することができません。なので、ローカル環境で Python をビルド、 テストファイルと一緒にホストマシンにアップロードすることで任意のバージョンを使用することができます。

ビルド済み Python ランタイムを Device Farm にアップロードする

ローカルで Python をビルドしましょう。誰でも同じ環境でビルドできるように Docker でビルド環境を作ると良いかと思います。 参考となるように、Dockerfileの内容を記載しておきます。

FROM amd64/ubuntu:14.04
ARG _python_ver=3.9.14
ARG _openssl_ver=1.1.1q
RUN apt update && apt install -y build-essential libbz2-dev libdb-dev libreadline-dev libffi-dev libgdbm-dev liblzma-dev libncursesw5-dev libsqlite3-dev libssl-dev zlib1g-dev uuid-dev wget
WORKDIR /usr/local/src
RUN wget http://artfiles.org/openssl.org/source/openssl-${_openssl_ver}.tar.gz && wget https://www.python.org/ftp/python/${_python_ver}/Python-${_python_ver}.tgz
RUN tar zxf openssl-${_openssl_ver}.tar.gz && tar zxf Python-${_python_ver}.tgz
WORKDIR openssl-${_openssl_ver}
RUN ./config -shared --prefix=/usr/local && make && make install_sw && mkdir lib && bash -c 'cp ./*.{so,so.1.1,a,map,pc} ./lib'
WORKDIR ../Python-${_python_ver}
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/lib
RUN ./configure --with-openssl=/usr/local/src/openssl-${_openssl_ver} --prefix=/tmp/python && make &&  make commoninstall bininstall
CMD ["/bin/bash"]

今回は Airtest がサポートしている 3.9 を採用しました。*1 ビルドされたpythonはDockerコンテナのtmpディレクトリ内に入っているのでローカルにコピーし、以下のような配置にしましょう。

テストバンドルのルートフォルダ
├── python
│   ├── bin
│   │   ├── pip
│   │   ├── pip3
│   │   ├── pip3.9
│   │   ├── python -> python3.9
│   │   ├── python3 -> python3.9
│   │   ├── python3.9
│   │   └── wheel
│   ├── include
│   │   └── python3.9
│   └── lib
│       ├── libpython3.9.a
│       ├── pkgconfig
│       └── python3.9
├── ssl
│   ├── libcrypto.so.1.1
│   └── libssl.so.1.1

ビルド後のファイル群はテストファイルと一緒にzipファイルにして、ホストマシンにアップロードします。 そして、 testspec.yaml でアップロードした Python に対してパスを通します。また、Device Farm 側の環境にはOpenSSL1.0.2(のみ)が入っているため、OpenSSL1.1系でPythonをビルドした場合は Python Runtime だけでなくOpenSSL1.1のライブラリも アップロードします。その上で LD_LIBRARY_PATH に含める必要があります。

version: 0.1

phases:
  install:
    commands:
      - export PYTHON_VERSION=3
      - cd $DEVICEFARM_TEST_PACKAGE_PATH
      - cp -r ./python /tmp
      - export LD_LIBRARY_PATH=$DEVICEFARM_TEST_PACKAGE_PATH/ssl:/usr/local/lib:/usr/lib
      - export PATH="/tmp/python/bin:$PATH"
      - pip install --upgrade pip
      - pip install -r requirements.txt
  pre_test:
    commands:
      - cd $DEVICEFARM_TEST_PACKAGE_PATH
      - export PYTHONHOME=/tmp/python
      - export PYTHONVER=3.9
      - export ADBPATH=${PYTHONHOME}/lib/python${PYTHONVER}/site-packages/airtest/core/android/static/adb/linux/adb
      - export SNIPPETPATH=${PYTHONHOME}/lib/python${PYTHONVER}/site-packages/airtest/utils/snippet.py
            - echo "adb setup start"
      - rm $ADBPATH
      - adb_path=$(which adb)
      - cp $adb_path $ADBPATH
      - sed -i -e s#^\$ADB#$adb_path.orig#g $ADBPATH
          - echo "Comment out python bugfix code by Airtest"
      - sed -ie "80,81 s/^/#/" $SNIPPETPATH
      - echo "Pre Test. OK"

# 以下省略

Pythonバージョンに起因した Airtest, Device Farm の問題

subプロセス終了判定問題

特定のPythonのバグの影響で子プロセスの終了判定がうまくいかず、 Device Farm のテストプロセスが正常に終了しない問題があり、Airtest 側でこれを独自に対応しています。このバグが修正されたPython実行環境でテストを実行するとテストランが正常に終了しないため、一時的に対応コードをコメントアウトする対応を行っています。

- sed -ie "80,81 s/^/#/" $SNIPPETPATH

Rotation Watcherの終了問題

以下の記事にあるように、一部のPythonバージョンではテスト終了後に rotationwatcher の終了が正常に行われず、Device Farm のテストプロセスがRunning状態のままになる問題があります。

hackerslab.aktsk.jp

Device Farm 側の標準のPythonバージョン3.7.4ではこの問題が発生するため、上記の記事にあるような対応が必要となりますが、3.8.13以降ではこの対応は不要です。これも Device Farm 標準のPythonを使用しない利点になります。

Device Farm のPythonバージョンがアップデートされると解決する(そもそもPythonアップロードから不要になる)ので、ユーザとしては切に願っています。

Local の Python 実行環境について

Device Farm のPythonバージョンを固定したので、 Local の Pythonバージョンもそれに合わせるべきです。また、開発者が複数人いる場合は Pipenv で Python 実行環境をまるっと管理した方が良いと思います。Poetry という手段もありますが、 Airtestのパッケージとの依存関係を解決できないので現時点(2022-12)では使用できませんでした。Pipenv と Poetry の依存関係解決アルゴリズムの違いから発生していると思われます。

testspec.yaml の一部コードを変更します。 requirements.txt ではなく Pipfile からパッケージインストールします。 Pipfile Pipfile.lock は Device Farm にアップロードしてください。requirements.txt は本来アップロード不要なんですが、 Appium Python と認識させるために Device Farm の仕様上必要です。

version: 0.1

phases:
  install:
    commands:
      # 省略

      - pip install --upgrade pip
      # - pip install -r requirements.txt
            - pip install pipenv
            - PIPENV_VENV_IN_PROJECT=1 pipenv install --system

Local の IDE の Python Runtime 設定

IDE で IDE付属の Python Runtime 以外を利用する場合は Airtest IDE のOptions > SettingsCustom Python Path の設定が必要です。ここで設定するのは Pipenv で用意した Python のパスです。

Python Runtime カラムに結果反映

Python Runtime カラムに上で説明した pipenv とセルフビルドを反映します。

実行環境 実行方法 ファイル拡張子 Python Runtime ランチャー
Local Airtest IDE .air IDE付属(3.6.6) 標準
Local Airtest IDE .air IDE付属(3.6.6) カスタム
Local Airtest IDE .py IDE付属(3.6.6) -
Local Airtest IDE .air pipenv(3.9.14) 標準
Local Airtest IDE .air pipenv(3.9.14) カスタム
Local Airtest IDE .py pipenv(3.9.14) -
Local Airtest CLI .air pipenv(3.9.14) 標準
Local Python .py pipenv(3.9.14) -
AWS Device Farm AWS CLI → Python .py セルフビルド(3.9.14) -

テストスクリプト実行命令環境

テストスクリプトを実行する環境は Local や Device Farm に整いました。開発者だけが Device Farm で実行する場合は Local PC からawsコマンドを実行すれば良いのですが、自動テストは誰でも簡単に実行できるように作った方が良いです。

そのために、 Jenkins ジョブで Device Farm のテスト実行をできるようにします。環境は増えましたが、Jenkins でも Local でもやっていることに違いはありません。最終的に aws コマンドを実行しているだけです。意識する点は、 Jenkins ジョブで設定するパラメータ(環境変数)を Local でどのように簡単に設定するか、かと思います。

ひとまず、これまでの表に実行命令環境を加えます。Jenkins の行が1行増えました。

実行命令環境 実行環境 実行方法 ファイル拡張子 Python Runtime ランチャー
Local Local Airtest IDE .air IDE付属(3.6.6) 標準
Local Local Airtest IDE .air IDE付属(3.6.6) カスタム
Local Local Airtest IDE .py IDE付属(3.6.6) -
Local Local Airtest IDE .air pipenv(3.9.14) 標準
Local Local Airtest IDE .air pipenv(3.9.14) カスタム
Local Local Airtest IDE .py pipenv(3.9.14) -
Local Local Airtest CLI .air pipenv(3.9.14) 標準
Local Local Python .py pipenv(3.9.14) -
Local AWS Device Farm AWS CLI → Python .py セルフビルド(3.9.14) -
Jenkins AWS Device Farm AWS CLI → Python .py セルフビルド(3.9.14) -

Local での環境変数の設定方法

Device Farm の実行が Jenkins でしか行えないとなるとちょっと不便です。機能開発したり、不具合修正したりするときに、サクッと Local から Device Farm を実行したいものです。Jenkins ではジョブ実行時のパラメータは環境変数として扱われますが、 Local でも同様の環境変数を設定できるようになっていないとサクッとは行きません。Device Farm は AWS CLI で実行可能ですが、その前にやることがたくさんある(テストに使用するファイルのアップロードとか)のでシェルスクリプトに処理をまとめて、実行するようにしています。なので、以下のようにシェルスクリプトに対して環境変数を渡すことは可能です。

$ TEST_SCENARIO=smoke TIMEOUT=10 RUN_NAME=john-smoke ./release.sh

しかし、そもそも Jenkins で設定する環境変数の種類やdefault値が何かもよくわかりません。シェルスクリプトの中でdefault値は定義できますが、もう少しスマートにテスト実行したいものです。

そこで、タスクランナーとして invoke を導入します。invoke は Python でタスクを記述できるので、ちまちま実行しないといけないコマンドを1つのタスクで定義できます。また、コマンドオプションも定義できるので、今回のような Jenkins で設定される環境変数もコマンドオプションで簡単に書くことができるようになります。もちろん help オプションも作れるのでそれらを覚えておく必要もありません。

$ invoke test.df --scenario=smoke --timeout=10 --run-name=john-smoke
# invoke は inv で実行しても良い

また、タスクランナーを作っておくだけで、他の aws コマンドの実行やキャッシュファイルを消す作業など、開発者各々が勝手にタスク化してどんどん便利になるのでおすすめです。

invoke 導入後の実行方法の変化

実行命令環境 実行環境 実行方法 ファイル拡張子 Python Runtime ランチャー
Local Local Airtest IDE .air IDE付属(3.6.6) 標準
Local Local Airtest IDE .air IDE付属(3.6.6) カスタム
Local Local Airtest IDE .py IDE付属(3.6.6) -
Local Local Airtest IDE .air pipenv(3.9.14) 標準
Local Local Airtest IDE .air pipenv(3.9.14) カスタム
Local Local Airtest IDE .py pipenv(3.9.14) -
Local Local Airtest CLI .air pipenv(3.9.14) 標準
Local Local invoke → Python .py pipenv(3.9.14) -
Local AWS Device Farm invoke → AWS CLI → Python .py セルフビルド(3.9.14) -
Jenkins AWS Device Farm invoke → AWS CLI → Python .py セルフビルド(3.9.14) -

PYTHONPATH と PROJECT_ROOT と using について

システムが大きくなってくると、ディレクトリ構成をちゃんとしたくなってきます。テストスクリプトが増えてくると、共通処理はモジュール化したくなってくるでしょう。ただ、 Python にモジュールパスが通っていないと import ができないのでそういうときはプロジェクトのルートディレクトリにパスを通しましょう。そして各スクリプトの import 時は完全なパッケージ名で記述すると良いかと思います。パスを通すために環境変数で PYTHONPATH を定義し、ルートまでの絶対パスを入れておきましょう。

ちなみに、テスト自動化システムだけのローカル環境変数なので、 direnv を使って管理しています。

export PROJECT_ROOT="$(dirname $1)"
export PYTHONVER="$(_pythonver)"
PYTHONPATH="${PROJECT_ROOT}:${PROJECT_ROOT}/.venv/lib/python${PYTHONVER}/site-packages"

また、Airtestでは PROJECT_ROOT という環境変数を定義しておくと、テスト実行前に Settings クラスで自動的に取り込まれて、ルートパスとして認識されます。

airtest.core.settings

class Settings(object):
    PROJECT_ROOT = os.environ.get("PROJECT_ROOT", "")  # for ``using`` other script

この PROJECT_ROOT は 主に using 関数で用いられています。

airtest.core.helper

def using(path):
    if not os.path.isabs(path):
        abspath = os.path.join(ST.PROJECT_ROOT, path)
        if os.path.exists(abspath):
            path = abspath
    G.LOGGING.debug("using path: %s", path)
    if path not in sys.path:
        sys.path.append(path)
    G.BASEDIR.append(path)

using の中身は sys.path.append しているだけです。ルート直下だけパスを通しておけばどこでも参照可能なので不要かと思われますが、 Airtest の世界では必要となる場面があります。それが .air で定義されているテストシナリオの import です。

# 【間違った例】 scenario/smoke.air/smoke.py の中の Smoke クラスを使用したい場合
from scenario.smoke.air.smoke import Smoke

上の表記では .air があるせいでパスを解釈できず、 import に失敗します。この場合に using を使用します。

# 【正しい例】 scenario/smoke.air/smoke.py の中の Smoke クラスを使用したい場合
from airtest.core.api import using
using("scenario/smoke.air")
from smoke import Smoke

これで smoke.air までパスが通っているので、 Smoke クラスを利用することができます。

using は簡単にパスを通せてしまうので、多用すべきでは無いと思います。基本的には、ルート直下にのみパスを通して完全なパッケージ名で記述する方がシンプルです。 .air の import のみに留めましょう。

PYTHONPATH 環境変数が反映される環境とは

先程から継ぎ足されている表の中で PYTHONPATH が反映される環境はどこでしょうか?前提として、 Local では pipenv の仮想環境内(direnv によって PYTHONPATH が定義されている世界)でテスト実行することとします。特に IDE は direnv が実行されているシェルで open コマンドを使って起動してください。 表の中に PYTHONPATH カラムを追加しました。

実行命令環境 実行環境 実行方法 ファイル拡張子 Python Runtime ランチャー PYTHONPATH
Local Local Airtest IDE .air IDE付属(3.6.6) 標準 No
Local Local Airtest IDE .air IDE付属(3.6.6) カスタム No
Local Local Airtest IDE .py IDE付属(3.6.6) - No
Local Local Airtest IDE .air pipenv(3.9.14) 標準 Yes
Local Local Airtest IDE .air pipenv(3.9.14) カスタム Yes
Local Local Airtest IDE .py pipenv(3.9.14) - Yes
Local Local Airtest CLI .air pipenv(3.9.14) 標準 Yes
Local Local invoke → Python .py pipenv(3.9.14) - Yes
Local AWS Device Farm invoke → AWS CLI → Python .py セルフビルド(3.9.14) - Yes
Jenkins AWS Device Farm invoke → AWS CLI → Python .py セルフビルド(3.9.14) - Yes

Python Runtime がIDE付属の場合は PYTHONPATH が反映されていません。IDE付属の Python は仮想環境とは別のディレクトリに配置されているからだと思われます。 Local 上のカスタムPythonは .venv の中にあるので反映されています。また、Device Farm 上は testspec.yaml

- export PYTHONPATH=$DEVICEFARM_TEST_PACKAGE_PATH

を記述して反映させています。余談ですが、 $DEVICEFARM_TEST_PACKAGE_PATH のように Device Farm では利用できる環境変数がいくつか用意されています。

最終的に対応すべき環境とは

上の表を改めて見ると、環境の多さに震えます。この環境全てに対応し、動作確認を楽に行える状態に持っていくのは正直辛いです。なので、開発しづらい環境は予め除外してしまいましょう。

開発しづらい環境の指標

  • PYTHONPATH が使えない環境
    • using を使うために別手段で PYTHONPATH を定義しないと行けない
  • ランチャーが標準の環境
    • テスト前後の共通処理はランチャーに書きたい
  • IDEでランチャーが使えない .py 実行環境
    • ランチャーに色々書きたいのにそれが使えないのは辛い

結果的に対応すべき環境は4つ(Device Farm 実行環境は Local/Jenkins で同じなので実質3つ)に絞られます。

実行命令環境 実行環境 実行方法 ファイル拡張子 Python Runtime ランチャー PYTHONPATH 対応すべき環境
Local Local Airtest IDE .air IDE付属(3.6.6) 標準 No ×
Local Local Airtest IDE .air IDE付属(3.6.6) カスタム No ×
Local Local Airtest IDE .py IDE付属(3.6.6) - No ×
Local Local Airtest IDE .air pipenv(3.9.14) 標準 Yes ×
Local Local Airtest IDE .air pipenv(3.9.14) カスタム Yes
Local Local Airtest IDE .py pipenv(3.9.14) - Yes ×
Local Local Airtest CLI .air pipenv(3.9.14) 標準 Yes ×
Local Local invoke → Python .py pipenv(3.9.14) - Yes
Local AWS Device Farm invoke → AWS CLI → Python .py セルフビルド(3.9.14) - Yes
Jenkins AWS Device Farm invoke → AWS CLI → Python .py セルフビルド(3.9.14) - Yes

対応すべき環境の表(まとめ)

だいぶシンプルになりましたね。なんだか行けそうな気がしてきました。

実行命令環境 実行環境 実行方法 ファイル拡張子 Python Runtime ランチャー
Local Local Airtest IDE .air pipenv(3.9.14) カスタム
Local Local invoke → Python .py pipenv(3.9.14) -
Local/Jenkins AWS Device Farm invoke → AWS CLI → Python .py セルフビルド(3.9.14) -

テストを実行する手段を検討する

Airtest が何を使ってテストを実行しているのか

テストは Airtest が実行するものだといつから錯覚していましたか?IDE に関して言うと、 Airtest を使ってテスト実行していると言えますが、実行方法が Python になるとどうでしょうか。

ここで、Airtest が何を使ってテストコードを実行しているのか見てみます。

airtest/cli/runner

def run_script(parsed_args, testcase_cls=AirtestCase):
    global args  # make it global deliberately to be used in AirtestCase & test scripts
    args = parsed_args
    suite = unittest.TestSuite()
    suite.addTest(testcase_cls())
    result = unittest.TextTestRunner(verbosity=0).run(suite)
    if not result.wasSuccessful():
        sys.exit(-1)

Airtest はPython標準のテストフレームワークである unittest を実行していることがわかります。Airtest CLI ではこの run_script が呼ばれ、テストが実行されます。Airtest IDE は内部ソースコードが公開されていないので詳細を追えていませんが、おそらく同様の処理が行われているのではないかと思います。

pytest という選択肢

対応すべき環境の表を見ると、IDE 以外で実行するテストスクリプトは .py です。どうやってテストをするのかを決めているのはもちろん Airtest ですが、何を使ってテスト実行するのかは .py である以上自由に決めることができます。最小構成だと、これでもテストはできると思います。

$ python smoke.py

ただ、テスト実行するにしてもテスト前処理、後処理、テストケースごとの処理はうまく書きたいでしょうし、テスト失敗時の情報は充実していた方がよいでしょう。そういうのをうまいことやってくれるのがテストフレームワークなので、使わない手はないです。IDE の場合 custom_launcher.py でテスト前後の処理などはカスタムできますが、Pythonのテストで使われているフレームワークと比較すると物足りなさを感じます。

せっかく対応すべき環境の中にPython実行が含まれているので、Pythonテストフレームワークを導入し、楽にテスト実行できるようにしてみましょう。となると unittest や pytest, nose とかになるんですが、技術的にはなんでも良いかと思います。Device Farm では Appium Python と認識させてテスト実行させていますが、そこでは pytest でテスト実行されているようです。なので、ここでは気分で pytest を選択します。

実行命令環境 実行環境 実行方法 ファイル拡張子 Python Runtime ランチャー
Local Local Airtest IDE .air pipenv(3.9.14) カスタム
Local Local invoke → pytest .py pipenv(3.9.14) -
Local/Jenkins AWS Device Farm invoke → AWS CLI → pytest .py セルフビルド(3.9.14) -

.py のランチャーについて

.air のカスタムランチャーとして custom_launcher.py ファイルを作成しています。pytest を導入したので .py のランチャーも作成可能になりました。ここではそのファイルを test_main.py とします。つまり、IDE(.air) → custom_launcher.py、CLI(.py) → test_main.py が対をなす存在になります。IDE, CLI ともに実行したいコードはそれぞれのファイルに記述しますが、どちらか一方のみ必要な処理は対応するファイルに記述することでキレイに整理することができます。

ここで表のランチャーを具体的なファイル名にします。

実行命令環境 実行環境 実行方法 ファイル拡張子 Python Runtime ランチャー
Local Local Airtest IDE .air pipenv(3.9.14) custom_launcher.py
Local Local invoke → pytest .py pipenv(3.9.14) test_main.py
Local/Jenkins AWS Device Farm invoke → AWS CLI → pytest .py セルフビルド(3.9.14) test_main.py

テストフレームワークを導入することのメリット

pytest を使ってテスト実行していますが、作っているテスト自動化システムそのもののテストにも pytest が使えるというメリットがあります。 テストするものをテストすることは結構重要だと思います。pytest でなくても良いですが、何らかのテストフレームワークは導入しておくとテストが書きやすくなります。

Unity Editor をデバイスとして認識させる

Airtest では Unity Editor を操作対象のデバイスとして認識させることができます。ただし、この機能は Windows に限定されています。 connect_device で application を設定すると Unity Editor と接続できます。

# connect local windows application
connect_device("Windows:///?title_re=unity.*")

https://airtest.readthedocs.io/en/latest/README_MORE.html#connect-windows-application

また、Poco を使用している場合は UnityEditorWindow を device に設定することで本格的に Unity Editor をデバイスとして操作することができます。

# import unity poco driver from this path
from poco.drivers.unity3d import UnityPoco
from poco.drivers.unity3d.device import UnityEditorWindow

# specify to work on UnityEditor in this way
dev = UnityEditorWindow()

# make sure your poco-sdk component listens on the following port.
# default value will be 5001. change to any other if your like.
# IP is not used for now
addr = ('', 5001)

# then initialize the poco instance in the following way
# specifying the device object
poco = UnityPoco(addr, device=dev)

https://poco.readthedocs.io/en/latest/source/doc/drivers/unity3d.html?highlight=UnityEditorWindow#unityeditor-on-windows

Windows と Airtest IDE の組み合わせについて

Windows でも IDE や CLI の操作が可能なので表に追加します。デバイスホストマシンOSとデバイスを追加しました。

実行命令環境 実行環境 デバイスホストマシンOS デバイス 実行方法 ファイル拡張子 Python Runtime ランチャー
Local Local Windows/Mac Unity Editor/Android/iOS Airtest IDE .air pipenv(3.9.14) custom_launcher.py
Local Local Windows/Mac/Linux Unity Editor/Android/iOS invoke → pytest .py pipenv(3.9.14) test_main.py
Local/Jenkins AWS Device Farm Linux Android/iOS invoke → AWS CLI → pytest .py セルフビルド(3.9.14) test_main.py

Windows x Airtest IDE で PYTHONPATH, PROJECT_ROOT 環境変数が認識できない問題

今までは Pipenv の仮想環境上で IDE を起動していたので PYTHONPATH や PROJECT_ROOT が読み込めていましたが、Windows では direnv が利用できないので環境変数が定義されていません。その場合、CLI と同じテストスクリプト実行しようとすると、モジュールへのパスが通っていないので import error になります。環境変数を定義する方法はたくさんありますが、direnv のようにローカルの世界で完結させたいです。

解決策として、Python プロジェクトで一般的に使われている pyproject.toml に PYTHONPATH や PROJECT_ROOT の代わりとなるパラメータを定義します。そして、そのパラメータを custom_launcher.py で読み込み、 sys.path.append するとルート直下にパスが通るので CLI と同じテストスクリプトを実行することができます。

pyproject.toml

[autotest]
project_root = "path/to/project_root"
python_path = ["path/to/project_root", "path/to/project_root/.venv/lib/python3.9/site-packages"]

まとめ

これでようやく自動テストを開発しやすい環境が整ったと思います。

こんなに多い実行環境が、

実行命令環境 実行環境 実行方法 ファイル拡張子 Python Runtime ランチャー PYTHONPATH 対応すべき環境
Local Local Airtest IDE .air IDE付属(3.6.6) 標準 No ×
Local Local Airtest IDE .air IDE付属(3.6.6) カスタム No ×
Local Local Airtest IDE .py IDE付属(3.6.6) - No ×
Local Local Airtest IDE .air pipenv(3.9.14) 標準 Yes ×
Local Local Airtest IDE .air pipenv(3.9.14) カスタム Yes
Local Local Airtest IDE .py pipenv(3.9.14) - Yes ×
Local Local Airtest CLI .air pipenv(3.9.14) 標準 Yes ×
Local Local invoke → Python .py pipenv(3.9.14) - Yes
Local AWS Device Farm invoke → AWS CLI → Python .py セルフビルド(3.9.14) - Yes
Jenkins AWS Device Farm invoke → AWS CLI → Python .py セルフビルド(3.9.14) - Yes

ここまでシンプルになりました。

実行命令環境 実行環境 デバイスホストマシンOS デバイス 実行方法 ファイル拡張子 Python Runtime ランチャー
Local Local Windows/Mac Unity Editor/Android/iOS Airtest IDE .air pipenv(3.9.14) custom_launcher.py
Local Local Windows/Mac/Linux Unity Editor/Android/iOS invoke → pytest .py pipenv(3.9.14) test_main.py
Local/Jenkins AWS Device Farm Linux Android/iOS invoke → AWS CLI → pytest .py セルフビルド(3.9.14) test_main.py

IDE, CLI ともに同一の自動テストスクリプトを実行するために以下のことを行いました。

  • Pipenv を導入し、IDE, CLI ともに Python Runtime を固定する
  • PYTHONPATH 環境変数を定義し、パスを通す
  • テストスクリプトのランチャーファイルを作成し、それぞれの環境特有の処理はそのファイル内で済ませる
  • Windows x Airtest IDE で 環境変数を読み込める仕組みを作る

また、Device Farm をテスト実行環境にするためのノウハウも紹介しました。

  • Airtest を Device Farm で利用するための方法
  • セルフビルドした Python を利用する方法

更に、効率良く開発するために利用したツールも紹介しました。

  • タスクランナー: invoke
  • テストフレームワーク: pytest

Device Farm までを視野に入れたテスト自動化を行うのであれば参考になる情報は多いと思いますが、例えば Local にあるサーバルームのPCでエミュレータを使ってテストする場合 invoke は大仰かもしれません。ただ、カスタムランチャーファイルを作ったり、パスを通したりする部分は参考になると思います。

このテスト自動化システムは今も開発を続けており、残課題もあります。運用に乗せきれていないので最適解かどうかもまだわかりません。今後もっといい方法が見つかる可能性も大いにあります。

私としてはこの記事を読んで、テスト自動化に取り組む方が増えると嬉しいなと思います。そして、Airtest や Device Farm などのテスト自動化技術の発展に繋がるとなお嬉しいです。

最後に

私の所属するチームでは、この記事で紹介したようなテスト自動化システムを開発しています。そして、アプリケーションのQA自動化を実現しようとしています。もしこの分野に興味のあるQAエンジニアの方がいましたらぜひカジュアルに面談しましょう。私の Twitter(@tomo_tk11) までお気軽に DM 飛ばしてください!

*1:ちなみに、 AirtestがサポートしているPythonバージョンの情報は中国語ドキュメントのみに書かれていて英語ドキュメントには書かれていません。