LLMを使ってモバイルアプリのUITestを自動化してみたい

はじめに

はじめまして。JCBデジタルソリューション開発部の若林です。MyJCBアプリのiOSエンジニアをしています。

今回は、モバイルアプリのUIテスト自動化ツール Maestro と LLMを組み合わせて、 「自然言語で書いた指示から、Maestroの操作コマンド(YAML)を生成して実行する」仕組みを試作してみました。

背景と課題

今回の取り組みは、「E2EテストにLLMを利用して、少しでも保守や実装の工数を削減してみたい!」という思いからスタートしました。

E2Eテストには大きく分けて「テストの実行」と「結果の確認」の2つの工程があると思いますが、今回はまず「テストの実行」の自動化にフォーカスしています。 モバイルアプリのテスト自動化において、以下のような点に課題を感じていました。

  • UIテストが不安定になりやすい
    • XCUITestで、対象要素の識別子(Accessibility Identifierなど)が取得できず、タップできずに失敗する
    • 画面変更・文言変更・レイアウト変更の影響を受けやすく、保守コストが高い
  • iOS/Androidで別々にテストを書いており、同じシナリオでも実装と保守が2倍になっている

そこで、画面上のテキストベースでの要素指定が得意であり、かつYAMLベースでiOS/Android両方の操作を共通化して書ける Maestro に注目しました。
これにより、OS間のテストコード二重管理問題や、内部的な識別子への過度な依存はある程度解消されます。
ただし、Maestroでも基本的には「どの要素を操作するか」を idtext などで明示的に指定する必要があり、 アプリ側のUI変更に引きずられる課題(=テストが落ちる/直し続ける)は残ります。

この状況を踏まえて、次の仮説を立てました。

  • 画面の構造(View Hierarchy)をLLMに渡せば、 人が自然言語で「何をしたいか」を書くだけで、 LLMが対象要素を推定してMaestro操作(YAML)を生成できるのではないか

この仮説を検証するために、冒頭で述べた仕組みを試作しました。

仕組み

Pythonスクリプトを介して、次の流れで実行します。

  1. 画面解析: maestro hierarchy で現在の画面階層を取得
  2. LLM推論: 「画面データ」+「ユーザーの指示(例:『ログインボタンをタップ』)」から、実行すべきMaestro YAMLを生成
  3. 後処理: LLMの出力に含まれうるMarkdownのコードフェンス(例:```yaml)などを除去し、YAMLのみの形に整形
  4. 実行: 生成したYAMLを一時ファイルに書き出して maestro test で実行

今回はLLM側は検証を実施した時点(2026年1月時点)でのコストパフォーマンスの良さを考えてGemini CLIとgemini-2.5-flashを利用してみましたが、copilot CLIやcodex CLIでも同様の仕組みは作れると思います!

後続で結果の詳細については説明しますが、精度・速度共にgemini-2.5-flashで十分なパフォーマンスを発揮しました。

実装の詳細

以下が実装の詳細になります。
※なお、コードはLLMに生成させたものとなっています。また、今回の仕組みのコアとなる検証部分を抜粋したものです。実際のテスト運用ではLLMの出力揺れに対するガードレール的な実装や例外処理などが必須となりますが、本検証の主旨に直接影響しない実装については省略しています。

画面構造の取得

Maestroの maestro hierarchy コマンドで取得できるJSONを抜き出します。

LLM呼び出し

次に、LLMを呼び出してJSONとプロンプトを渡します。

def ask_gemini_cli(instruction, screen_context):
    prompt_text = f"""
あなたはモバイルアプリの自動テストエンジニアです。

[ユーザーの指示]
{instruction}

[画面の要素リスト]
{screen_context}

[回答のルール]
- IDがある場合: - tapOn: {{ id: "resource-id" }}
- テキストの場合: - tapOn: "Text String"
- YAMLのみ出力すること
"""

    cmd = ["gemini", "--prompt", prompt_text]
    result = subprocess.run(cmd, capture_output=True, text=True, check=True)
    return result.stdout.strip()

実行部分(生成したYAMLをMaestroで実行)

LLMから返ってきたYAML(例:- tapOn: "ログイン")を一時ファイルに保存し、maestro test で実行します。 一時ファイルは、古いコマンドの誤再実行を避けるため、finally を使って削除しています。

def run_smart_tap(user_instruction):
    # 1. 画面情報の取得
    raw_hierarchy = get_hierarchy_text()

    def normalize_maestro_yaml(text: str) -> str:
        # LLMが ```yaml ... ``` のようなMarkdownコードブロックで返すことがあるため除去
        return (
            text.replace("```yaml", "")
            .replace("```yml", "")
            .replace("```", "")
            .strip()
        )

    # 2. LLMでコマンド生成
    response_text = ask_gemini_cli(user_instruction, screen_context)

    # 3. 出力から不要なMarkdown記号などを除去
    command = normalize_maestro_yaml(response_text)

    # 4. 生成されたコマンドを実行
    print(f"生成コマンド: {command}")
    if command:
        yaml_filename = "temp_action.yaml"
        try:
            with open(yaml_filename, "w", encoding="utf-8") as f:
                f.write(f"appId: ~~~~~~\n---\n{command}")

            subprocess.run(["maestro", "test", yaml_filename])
        finally:
            if os.path.exists(yaml_filename):
                os.remove(yaml_filename)

使ってみた結果

これにより、例えば次のような指示だけで画面操作まで到達できました。

run_smart_tap("ログインって書いてあるボタンをタップして")
run_smart_tap("安心タブをタップして")

結果として、「画面の構造をLLMに渡せば、人が自然言語で指示するだけで、LLMが対象要素を推定してMaestro操作を自動生成・実行できる」という冒頭の仮説は、十分に実証できたと言えます。

今後の課題と活用アイデア

実際にMyJCBアプリのE2Eテストへ本格的に導入しようと考えると、「結果確認のアサーション(期待値通りかのチェック)はどうするのか?」など、まだまだ越えるべき課題はたくさんあります。

しかし、以下のような使い方であれば、現段階でも現実的かつ効果的に開発をサポートしてくれるのではないかと考えています。

  • テストコード生成のアシスタントとして使う

    • 毎回のテスト実行時にLLMに推論させるのではなく、テスト作成時に「自然言語 → Maestro YAML」の変換ツールとして使い、出来上がったYAMLを確定版のテストファイルとして保存・運用する。
  • 開発中の動作確認のための「使い捨てシナリオ」として使う

    • 「特定の画面まで飛ばして動作確認したい」といった、開発中の手作業を一時的に自動化するために利用する。

おわりに

今回は、MaestroとLLMを組み合わせて「自然言語の指示からUI操作を自動生成・実行する」仕組みを試作してみました!

本格的なE2Eテストの完全自動化にはまだステップが必要ですが、テストコード生成の補助やちょっとした動作確認の自動化など、用途を絞って取り入れることで十分に開発体験を向上させられそうだと感じています。

この記事が、モバイルアプリのテスト自動化に悩む方のちょっとしたヒントになれば嬉しいです!


最後に、JCBでは我々と一緒に働きたいという人材を募集しています。 詳しい募集要項等については採用ページをご覧下さい。


本文および図表中では、「™」、「®」を明記しておりません。 記載されているロゴ、製品名は各社及び商標権者の登録商標あるいは商標です。

©JCB Co., Ltd. 20︎21