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

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

AWS Device Farm を使って Airtest を実行するときのフローとは

この記事は ソフトウェアテスト Advent Calendar 2022 14日目の記事です。

はじめに

はじめまして、アカツキゲームスでQAエンジニアをやっている山﨑@tomo_tk11です。私は現在、モバイルゲームアプリケーションの自動テストシステムの開発を担当しています。ゲームの自動テストといえば NetEase 社から提供されている Airtest が有名です。CEDEC でもいくつか事例が発表されており*1、私たちも Airtest を用いてテスト自動化を試みています。

実は、 Akatsuki Games Advent Calendar 2022 の5日目 に Airtest x AWS Device Farm を用いた自動テストの実行方法をまとめたのですが、そこでは書ききれなかったことがあるので別記事にしようと思い立ちました。

この記事では、自動テストの実行フローと、実行時に必要なパラメータ設定タイミングについてまとめています。

この記事を楽しんでいただくためのヒント

本記事は、以下の Airtest x AWS Device Farm を用いた自動テストの実行方法についてまとめた記事が前提となっています。この記事単体でも話が成立するように書いてはいますが、前提となる記事を読むことで理解の助けになると思います。

hackerslab.aktsk.jp

目次

ターゲット

  • Airtest(and Poco) を使用してテスト自動化を行っている方
  • AWS Device Farm を利用してテスト自動化を行っている方

この記事でわかること

  • AWS Device Farm からテストシナリオスクリプトを実行するまでのフロー
  • 各世界で必要なパラメータ
  • パラメータを設定するタイミング

AWS Device Farm x Airtest で動作保証すべき3つの環境

前提となっている記事の超要約です。

Airtest を Local で実行し、自動テストする場合はシンプルな実行環境になります。しかし、IDE, CLI の実行をそれぞれサポートしたり、Device Farm をテスト実行環境として採用したりすると、テスト実行手段が増えて自動化システムが複雑になります。結果として、すべての環境で動作保証がされなくなり、開発効率の低下に繋がります。

なので、テスト実行環境を整理し、最低限動作保証すべき環境を3つに絞りました。

環境を絞るまでの過程は Airtest で CLI, IDE や Device Farm, Local など様々なテスト実行手段、環境があったとしても楽に自動テストを実行する方法 に書いているので気になる方はご確認ください。

実行命令環境 実行環境 デバイスホストマシン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

自動テストの実行フローとパラメータ設定タイミング

実行方法はシンプルになったので、ここでは実行フローに焦点を当てて解説していきます。各実行方法でテスト実行し、どういうフローで共通のテストシナリオスクリプトにたどり着くのかを見ていきましょう。先に結論である全体図を貼ります。

自動テストシステムに存在する世界

  • Device Farm: 実行環境
    • Jenkins: Device Farm を楽に実行するための手段
  • Python: テストスクリプトの言語
    • Airtest: 自動テストフレームワーク及びテスト実行手段(IDE)
      • pytest: テスト実行手段(CLI)
      • ゲームアプリ: テスト実行対象

多くの世界が存在しており、それぞれに役割があります。そして、それぞれの世界で取り扱うパラメータが存在します。そのパラメータが設定されるタイミングや、管理方法を整理しないままシステムを構築すると複雑化の原因になります。

各世界のパラメータについて

今後増えるかもしれませんが、現時点でのパラメータを列挙します。一つ一つ解説していくと長くなるのでここではやりませんが、Device Farm や Airtest 世界のパラメータは独自ドメインのものが少ないので参考になるかと思います。PYTHONPATH や PROJECT_ROOT については 前提記事で解説しています。

  • Jenkins 世界のパラメータ
    • テスト管理リポジトリのブランチ名
  • Device Farm 世界のパラメータ
    • テスト実行対象のアプリバージョン
    • 起動前に配置するアセットバンドルサイズ
    • 実行時の Run 名
    • 実行するデバイスプール
    • タイムアウト時間(分)
  • Python 世界のパラメータ
    • モジュールパス(PYTHONPATH)
  • pytest 世界のパラメータ
    • 実行するテストシナリオ
  • Airtest 世界のパラメータ
    • プロジェクトルートパス(PROJECT_ROOT)
    • 操作ログの prefix
    • 操作ログ出力先のパス
    • レポート出力先のパス
  • ゲームアプリ 世界のパラメータ
    • アプリケーションパッケージ名
    • 接続するゲームサーバ名

Device Farm 世界のフローとパラメータ設定タイミング

Device Farm 実行から始まるフローは、動作保証すべき環境表の3行目に該当します。エンジニアだけが実行するのであれば Local でコマンドを叩くだけでも良いのですが、自動テストは誰でも実行できたり、特定の時間に実行できたりする必要があるので、 Jenkins ジョブを用意すると便利です。ここでは多くのパラメータを設定しますが、Device Farm を実行する作業はフローの末端に位置するので致し方ありません。特に Device Farm に関するパラメータが多数を占めています。Device Farm のパラメータはAWS CLI のコマンドや testspec.yaml ファイル内に反映されます。

invoke について

タスクランナーとして invoke を導入しています。invoke は Python でタスクを記述できるので、細かく実行しないといけないコマンドをまとめて1つのタスクで定義できます。以下の例では、 testspec.yaml の生成、AWS CLI の実行を行う run_device_farm.sh の処理を invoke タスクで行っています。シェルスクリプトに渡す環境変数をオプションとしてサクッと渡せるのは魅力的です。また、default 値も設定できる点も良いです。

また、 run_device_farm.sh の処理を Pythonとして記述してしまえば invoke 内に書くこともできるでしょう。

@task(
    help={
        "scenario": "Scenario to run",
        "environment": "Test environment",
        "df-run-name": "DF run session name",
        "df-device-type": "DF device type",
        "df-timeout": "DF session timeout min",
        "app-package": "Bundle ID of app under test",
    }
)
def df(
    c,
    scenario="smoke",
    environment="server01",
    df_run_name=None,
    df_device_type="Pixel5",
    df_timeout=5,
    app_package="app.package.name",
):
    """
    Run test in AWS Device Farm.
      Args:
        scenario: Scenario to run
        environment: Test environment
        df_run_name: DF run session name
        df_device_type: DF device type
        df_timeout: DF session timeout min
        app_package: Bundle ID of app under test
    """
    if not df_run_name:
        df_run_name = f"{os.getenv('USER')}-{scenario}"
    mp.invoke("Start running autotest in DF...")
    c.run(
        f"TEST_SCENARIO={scenario} \
        TEST_ENVIRONMENT={environment} \
        DF_RUN_NAME={df_run_name} \
        DF_DEVICE_TYPE={df_device_type} \
        DF_TIMEOUT={df_timeout} \
        APP_PACKAGE={app_package} \
        ./run_device_farm.sh",
        hide=False,
    )
    mp.invoke("Running autotest in DF: Completed!")

Airtest 世界のフローとパラメータ設定タイミング

Airtest IDE を使ってテスト実行するフローは動作保証すべき環境表の1行目、invoke → pytest を使って CLI でテスト実行するフローは2行目に該当します。

pytest addoption について

まず pytest から説明します。pytest はテストフレームワークなのでテストを効率よく行うための機能がたくさん備わっています。その中で今回は addoption に着目します。

def pytest_addoption(parser):
    """pytest実行時オプション"""
    parser.addoption("-S", "--scenario", action="store", help="実行するシナリオ")
    parser.addoption("-E", "--environment", action="store", help="接続先の環境")
    parser.addoption("-A", "--app-package", action="store", help="テスト対象アプリのBundle ID")

pytest_addoption でオプションを定義することで、pytest 実行時に値を渡せるようになります。その値は Python で書かれたテストスクリプト内で参照可能です。値参照が手軽にできるのはテストスクリプトと同じ言語のテストフレームワークを使う強みでもあります。

class TestMain:
        def test_main(self, main_context):
            """Test Main
          Args:
              main_context: Test run context
          """
            test_scenario = main_context.scenario
            test_environment = main_context.environment
            app_package = main_context.app_package

値は main_context から取得することができます。

pytest -s tests/test_main.air/test_main.py \
    --scenario smoke \
    --environment server01 \
    --app-package app.package.name

このように、pytest 実行時にオプションを渡します。本システムでは pytest は直接実行しておらず、 invoke を経由して pytest を実行しています。

Airtest IDE の Settings について

次に、Airtest IDE の実行フローについてです。IDE はフロー図から分かる通り、本流から外れた位置からの実行方法です。しかしながら、この IDE 実行においても CLI 実行と同様のテストシナリオスクリプトを動作させたいため、頑張って工夫します。

本流のフローでは、 pipenv で作られた仮想環境空間や Device Farm 内の Python 空間でテスト実行されるため、 PYTHONPATH を認識させるのは難しくありません。しかし、 IDE はその空間外でアプリケーションが起動しているため、普通にアプリケーションを開くと PYTHONPATH が定義されていない状態になります。もちろん global に環境変数を定義すれば認識可能ですが、それでは他の Python Project に影響を与えてしまいます。従って、以下の対応を行います。

  • IDE を pipenv の仮想環境空間からコマンドラインで開く(mac OS だと open コマンド)
  • IDE の Options > Settings の Custom Python Path に仮想環境空間内の Python Path を設定する(pipenv なので .pyenv のパス)

その他のパラメータ設定タイミング

フロー図には記載していませんが、Python Project のコンフィグ/設定ファイル(pyproject.toml)でもパラメータを設定できるようにしています。コンフィグファイルでは比較的静的な情報を定義すべきです。一応各パラメータをコンフィグファイルで設定できるようにしていますが、ここで全てを設定するのは非推奨です。

[autotest]
project_root = "/Users/YOUR_NAME/repos/name"
python_path = ["/Users/YOUR_NAME/repos/name", "/Users/YOUR_NAME/repos/name/.venv/lib/python3.9/site-packages"]
log_dir = "logs"
report_export_dir = "logs"
log_prefix = "autotest"
app_package = "app.package.name"
app_version = "1.0.0"
test_environment = "server01"
test_scenario = ["smoke"]

パラメータの基本方針

各世界で取り扱うパラメータは種類が多いのに加えて、上記のコンフィグファイルや環境変数、pytest のオプションなど管理方法も様々です。ここで、それぞれの管理方法の基本方針を定義します。

  1. プロジェクトレベルの比較的静的な情報はコンフィグ/設定ファイルで(pyproject.toml)で指定可能
  2. コンフィグより更新の頻度や可能性の高い設定値は環境変数でも指定可能(コンフィグの設定を上書きする)
  3. コンフィグ/環境変数よりも粒度の小さい個別実行単位などの指定にはオプション引数で指定(上記1, 2を上書き)

Setting クラスによるパラメータの集中管理

テストシナリオスクリプト内でそれらをバラバラに参照するわけにはいかないので、集中管理するための Setting クラスを作成します。そして、上記の基本方針も仕様に組み込みます。

class Setting:
    """
    テスト実行に必要な全設定をConfig/Environment/オプション引数などの全てのソースから集約して保持するデータクラス

    処理順や優先度は以下の通り。左から順に処理され以降に同じ設定があれば後の処理で上書きされていき、
    テスト実行前に必要な準備処理が終わった段階では全ての設定がSettingに保持されている。
    """

    def __init__(
        self,
        project_root=Config.project_root,
        python_path=Config.python_path,
        log_dir=Config.log_dir,
        log_prefix=Config.log_prefix,
        report_export_dir=Config.report_export_dir,
        app_package=Config.app_package,
        app_version=Config.app_version,
        test_environment=Config.test_environment,
        test_scenario=Config.test_scenario,
    ):
        self.project_root = Environment.PROJECT_ROOT or project_root
        self.python_path = Environment.PYTHONPATH if all(Environment.PYTHONPATH) else python_path
        self.log_dir = Environment.LOG_DIR or log_dir
        self.report_export_dir = Environment.REPORT_EXPORT_DIR or report_export_dir
        self.log_prefix = Environment.LOG_PREFIX or log_prefix
        self.app_package = Environment.APP_PACKAGE or app_package
        self.app_version = Environment.APP_VERSION or app_version
        self.test_environment = Environment.TEST_ENVIRONMENT or test_environment
        self.test_scenario = (
            Environment.TEST_SCENARIO if all(Environment.TEST_SCENARIO) else test_scenario
        )

Config クラスは toml ファイルから、Envrionment クラスは環境変数から値の読み込みを行っています。中身は単純なのでここでは説明は割愛します。ここで重要なのは、以下の2点です。

  • Setting クラスで値を集中管理できている
  • 管理方法ごとに優先度が付けられている
    • Config < Environment < Command line Option < Setting

これでテストシナリオスクリプトで値が欲しいときには、Setting クラスを参照するだけでよしなに値を取得することができるようになりました。

まとめ

この記事では、自動テストの実行フローとパラメータの設定タイミングについてまとめました。

結果として、それぞれの方法でテスト実行し、どういうフローで同一のテストシナリオスクリプトに到達するかを可視化することができました。また、自動テストの実行方法によって設定するパラメータに違いはありますが、invoke(タスクランナー) や pytest の addoption、 IDE の Settings、pyproject.toml(コンフィグファイル) を用いて解決することができました。更に Setting クラスを作り、パラメータの集中管理 + 優先度付けを行うことができました。

この記事によって、ゲームアプリケーションのテスト自動化に取り組まれる方が増えると私は嬉しいです。それでは、良いクリスマスを!

*1:CEDEC Airtest でググると何件かヒットします。サムザップさんの取り組みが有名です。