スマホのネイティブアプリのテスト自動化ってできないの?という相談を最近よく頂くのと、Playwrightのお勉強したいなあという個人的な願望もあり、導入だけやってみた備忘録を残します。
なお、Playwrightのネイティブモバイル機能は現在AndroidのADBプロトコルを利用したものに限定されているため、Androidのみの話になります。iOSユーザーの皆さんごめんなさい;;
Step 0: 準備編 - テストを始める前に
そもそもPCとAndroidスマホをどうやって繋ぐの?という部分のセットアップです。
1. Android SDK (adb) のセットアップ
adb
は、PCからスマホに命令を送るための必須ツールです。
-
入手方法: Android Studioの公式サイトから「Command line tools only」をダウンロードするのが一番手軽です。(Android Studio本体を全部インストールする必要はありません)
- Android Studio 公式ダウンロードページ (ページ下部にあります)
-
設定:
- ダウンロードしたzipファイルを解凍し、適当な場所(例:
C:\android-sdk
)に置きます。 - 中にある
platform-tools
というフォルダのパスを、Windowsの環境変数Path
に追加します。 - ターミナルを再起動し、
adb --version
と打ってバージョン情報が出れば成功です。
- ダウンロードしたzipファイルを解凍し、適当な場所(例:
2. Androidデバイスの開発者向けオプションを有効化
次にスマホ側の設定。PCからのデバッグ命令を受け付けられるようにします。
- スマホの「設定」→「デバイス情報」(または「タブレット情報」)を開きます。
- 「ビルド番号」という項目を、7回連続でタップします。(「デベロッパーになるまであと〇ステップです」と表示が変わっていきます)
- 「これでデベロッパーになりました!」と表示されたら成功です。
- 「設定」メニューに戻ると、新しく「開発者向けオプション」という項目が出現しているので、それを開きます。
- 中にある「USBデバッグ」と「ワイヤレスデバッグ」の両方をONにしておきましょう。
3. Playwrightプロジェクトの作成
最後に、今回の主役であるPlaywrightのプロジェクトを準備します。
# 適当な作業フォルダを作り、そこで以下を実行
npm init playwright@latest
いくつか質問されますが、基本はEnter連打でOKです。
これで、playwright.config.ts
や tests
フォルダなどが自動で作成されます。
Step 1: 接続確認と初テスト
準備が整ったので、いよいよPlaywrightとデバイスを繋いでみます。
1. 接続確立 (adb) - USBよりワイヤレスがおすすめ
ここでいきなり最初の壁にぶつかりました。当初、USBケーブルでPCとスマホを繋いで「USBデバッグ」を試したのですが、何度やってもadb devices
でデバイスを認識してくれませんでした(どなたか有識者いらっしゃればご教授賜りたい.....)
そこで「ワイヤレスデバッグ」に切り替えたところ一瞬で繋がりました。。
(重要:PCとスマホが同じWi-Fiなど、同一のネットワーク内にいる必要があります)
-
スマホの「開発者向けオプション」→「ワイヤレスデバッグ」を開きます。
-
「ペア設定コードによるデバイスのペア設定」をタップすると、IPアドレスとポート番号が表示されます。(例:
192.168.1.10:45678
) -
PCのターミナルで、表示されたIPとポートを使って接続コマンドを打ちます。
adb connect 192.168.1.10:45678 # ←スマホに表示されたIPとポートに置き換える
-
接続が成功したら、
adb devices
で確認します。adb devices # List of devices attached # 192.168.1.10:45678 device <- ワイヤレスデバッグだとIPアドレスで表示される
2. Playwrightから接続(初回テスト)
adb
で認識できれば、次はPlaywrightから接続できるか試します。
いきなりテストを実行しても、中身が空だと何も起きないので、まずはおまじないとして、tests/example.spec.ts
に以下の最小限のテストコードをコピペして保存します。
// tests/example.spec.ts
import { test, _android } from '@playwright/test';
test('デバイス接続テスト', async () => {
// デバイスに接続する
const [device] = await _android.devices();
console.log(`接続成功! モデル名: ${device.model()}`);
// デバイスとの接続を閉じる
await device.close();
});
この状態でテストを実行し、ターミナルにモデル名が表示されれば成功です!
npx playwright test
# 出力例:
# 接続成功! モデル名: Pixel 5
【ここでの学び】
- ADB接続で躓いたら、USB接続に固執せず、ワイヤレスデバッグを試してみるとあっさり解決することがある。
adb connect
コマンドを忘れずに!- Playwrightのテストは、まず「デバイスに接続して情報を表示するだけ」の最小構成で動かし、接続自体に問題がないことを確認するのが確実。
Step 2: 本命のネイティブアプリを起動する
ブラウザの自動化はよくあるので、本題のネイティブアプリに進みます。
1. パッケージ名を知る
アプリを操作する前に、そもそもアプリってどうやって識別するん?と思ったら、どうやら「パッケージ名」という固有IDで識別しているらしい。
これもadb
コマンドで調べられるらしいので調べてみます。
# インストールされてるアプリのパッケージ名を全部出す
adb shell pm list packages
# 多すぎるので、アプリ名で絞り込むのがおすすめ(PowerShellの場合)
adb shell pm list packages | Select-String "your-app-name"
# -> package:com.example.app ←これ
2. アプリの起動と「待機」
tests/example.spec.ts
の中身を、今度はアプリを起動するコードに書き換えていきます。
// tests/example.spec.ts
import { test, _android } from '@playwright/test';
test('ネイティブアプリ起動テスト', async () => {
const [device] = await _android.devices();
console.log(`モデル名: ${device.model()}`);
const pkg = 'com.example.app'; // ←さっき調べたアプリのパッケージ名に書き換える
// テスト前に一旦アプリを強制終了させて、まっさらな状態から始める
await device.shell(`am force-stop ${pkg}`);
// いざ起動!
await device.shell(`monkey -p ${pkg} -c android.intent.category.LAUNCHER 1`);
// ★超重要:待つこと★
// これがないと、アプリが立ち上がる前に次の処理に進んでしまう
await device.shell('sleep 5');
console.log('アプリを起動しました。');
await device.close();
});
ここでも見事にハマりました。起動コマンドの直後にスクリーンショットを撮ったら、まだホーム画面のままでした。コンピュータの処理は人間が思うより遥かに速いので、UIが絡む部分は意図的に待ちの処理を入れてあげる必要があります。
【ここでの学び】
UIの起動や画面遷移など、見た目が変わる操作の直後には、必ずsleep
や後述の「要素待機」を入れる。
Step 3: テストの証拠を残す
「動いた!」「動かなかった!」だけだと、後から見返したときに何が起きたか分からないので、エビデンスとしてスクリーンショットや録画は超重要です。
1. スクリーンショットを撮ってレポートに貼る
Playwrightのテストレポートに画像を添付すると、ブラウザで結果を見ながら画像も確認できて良きです。
tests/example.spec.ts
にさらに処理を加えていきましょう。
// tests/example.spec.ts
import { test, _android } from '@playwright/test';
import * as fs from 'fs';
test('ネイティブアプリ起動テスト', async ({}, testInfo) => { // ← testInfo を追加
// スクリーンショット保存用のフォルダを作成
fs.mkdirSync('screenshots', { recursive: true });
const [device] = await _android.devices();
console.log(`モデル名: ${device.model()}`);
const pkg = 'com.example.app';
await device.shell(`am force-stop ${pkg}`);
await device.shell(`monkey -p ${pkg} -c android.intent.category.LAUNCHER 1`);
await device.shell('sleep 5');
// スクショを撮る
const screenshotPath = 'screenshots/app-startup.png';
await device.screenshot({ path: screenshotPath });
// レポートに「起動直後」という名前で画像を添付
await testInfo.attach('起動直後', {
path: screenshotPath,
contentType: 'image/png'
});
console.log('アプリを起動し、スクリーンショットを撮りました。');
await device.close();
});
【ここでの学び】
テストの証拠は必ず残す。成功・失敗に関わらず、実行前後のスクリーンショットがあれば、問題解決のスピードが段違いになる。
Step 4: 最大の壁:ボタンが押せない問題との戦い
ここからが本番。アプリのダイアログの「次へ」ボタンを押そうとして、地獄のトラブルシューティングが始まりました。
試行1:ボタンのテキストでタップ (玉砕)
まず最初に試す、一番簡単な方法。普通に目に見えてるラベル名で押させてみる。
await device.tap({ text: '次へ' });
結果は「見つかりませんでした」。まあ、そんなもんだろうと思った。
試行2:リソースIDでタップ (推奨だが…玉砕)
テキストがダメなら、次はresource-id
。これは各UI要素に振られた固有のIDで、テキストより遥かに信頼性が高いです。
IDの調べ方 (コマンドライン)
-
adb shell uiautomator dump
を実行。 -
adb pull /sdcard/window_dump.xml .
でPCにファイルを持ってくる。 -
window_dump.xml
を開いて、text="次へ"
の近くにあるresource-id
を探す。
→com.example.app:id/next_button
を発見!
これを使って再挑戦。
await device.tap({ res: 'com.example.app:id/next_button' });
しかし、これも結果は「見つかりませんでした」。どうやらdevice.tap
とこのアプリのボタンの相性が悪いようです(ダイアログ上のボタンだから?)
試行3:uiautomatorコマンドを直接実行 (惜しい!)
Playwrightのtap
がダメなら、Androidのテストツールuiautomator
を直接コマンドで呼び出します。
const resourceId = 'com.example.app:id/next_button';
await device.shell(`uiautomator click --selector resourceId ${resourceId}`);
すると、テストログ上は「成功」しました!しかし、実際のデバイス画面ではボタンが押されていません。テスト自動化で一番厄介な「偽りの成功(False Positive)」ってやつです。
試行4:最終手段、座標でタップ (勝利!)
もう要素がどうとか言ってられません。最後の手段は、画面の特定の座標を物理的にタップすることです。
window_dump.xml
には、ボタンの表示領域bounds
も書かれています。
bounds="[125,1662][955,1777]"
この対角線の中心を計算すれば、ボタンのど真ん中の座標が分かります。
- X座標:
(125 + 955) / 2 = 540
- Y座標:
(1662 + 1777) / 2 = 1719.5
(まあ1719でいいでしょう)
これをinput tap
コマンドで実行します。
// まずは念のため、ボタンが存在することだけ確認
const resourceId = 'com.example.app:id/next_button';
await device.shell(`uiautomator dump && cat /sdcard/window_dump.xml | grep 'resource-id="${resourceId}"'`);
// 存在したら、座標を直接叩く!
await device.shell('input tap 540 1719');
これで、ついに「次へ」ボタンをタップすることに成功しました!
【学び(?)】
UI操作の自動化には「優先順位」がある。
- まず
tap
など高レベルなAPIを試す。- ダメなら
resource-id
を使う。- それでもダメなら
uiautomator
コマンドを直接叩く。- 最後の手段は
座標指定タップ
。
この引き出しの多さが、一筋縄ではいかないアプリの自動化を成功させる鍵となる。
まとめと今後の展望
というわけで、紆余曲折ありましたが、なんとかPlaywrightでAndroidネイティブアプリの基本的な操作(起動→ボタンクリック)までを自動化することができました。
正直、Webブラウザの自動化に比べると、UI要素の特定でハマる部分が多く、一筋縄ではいかない印象です。ただ、一度resource-id
や最終手段の座標タップ
まで経験してしまえば、大抵のことは何とかなるんじゃないか、という自信もつきました。
今後は、
- 今回作ったテストをベースに、ログインフォームへのテキスト入力(
device.fill()
)やアサーション(期待値のチェック)を追加して、もっと「テストらしい」テストに育てていく。 - 複数のテストケースを並列実行させてみる。
- CI/CDツール(GitHub Actionsなど)と連携させて、夜間に自動でテストが走る環境を構築する。
あたりに挑戦してみたいですね。
この備忘録が、これから同じ道を通る誰かの助けになれば幸いです。