Claude CodeでAppiumのモバイルE2Eテストを書く — 検証範囲の線引き
Claude CodeにAppium(UiAutomator2)のセッション設定とロケータを書かせる手順です。エミュレーターなし・実機なしで確かめられる範囲と、動かして初めて分かる範囲を分けます。
Claude CodeでAppiumのテストを書くとき、任せやすいのはセッション設定(capabilities)とロケータの書き分けです。どちらも「書式の決まり」と「画面の実物」の2つに分かれ、前者は手元で確かめられ、後者は端末で動かすまで分かりません。
この記事はAndroid(UiAutomator2ドライバー)とWebdriverIOの組み合わせで、Claudeに書かせる範囲と人が確かめる範囲の線引きを示します。
実機がなくても確かめられる範囲を先に分ける
Appiumのテストは、コードが正しくても端末が無ければセッションが始まりません。そこで検証を3段に分け、どの段までClaudeに閉じさせるかを決めておきます。
| 段階 | 必要なもの | Claudeに確かめさせられること |
|---|---|---|
| 1. 端末なし | 必要なものNode.jsとAppiumのみ | Claudeに確かめさせられること構文、capabilitiesの組み立て、appium:接頭辞の付け忘れ、appium driver doctorの結果 |
| 2. エミュレーター | 必要なものAVD(Android Virtual Device) | Claudeに確かめさせられることセッション開始、画面遷移、ロケータが要素に当たるか |
| 3. 実機 | 必要なものUSBデバッグを有効にした端末 | Claudeに確かめさせられること機種固有の挙動、adb devices -lにonlineで出るか |
段階1だけでも、Claudeに任せる価値は十分あります。capabilitiesの書き間違いは、端末につないでから初めてセッション開始のエラーとして出ます。書く段階でClaudeに潰させておけば、端末を用意してから書き直す手間を1回分省けます。
一方、ロケータが実際の画面に当たるかは、段階2以降でないと判定できません。ここを「書けたから通るはず」と読むのが、モバイルE2Eで最も多い手戻りの原因です。ロケータの当たり外れを端末なしで保証する手段は、Appiumの資料に載っていません。
前提を揃える — Appium 3とUiAutomator2ドライバー
環境の要件は、Claudeに書かせる前に人が揃えます。Appiumはnpmでグローバルに入れ、ドライバーは別に入れます。
npm install -g appium
appium driver install uiautomator2
appium driver doctor uiautomator2appium driver doctor uiautomator2は前提条件の検証ツールです。必須の修正が0件と出れば、設定は済んでいます。オプション項目の指摘は出ても構いません。
前提として押さえる点は次のとおりです。
- Appium 3はNode 20.19.0以上とnpm 10以上が必要です
- UiAutomator2ドライバーの5.0.0以降はAppium 3専用です
- ドライバー6.0.0以降のAndroidは、最小がAPI 26(Android 8.0)です
ANDROID_HOMEとJAVA_HOMEの環境変数を設定します。JavaはAndroid SDKがAPI 30未満なら8、API 30以上なら9以降が必要です- エミュレーターを使うなら、AVDとシステムイメージを用意します
- iOSを対象にするXCUITestドライバーはmacOSが必要です
XCUITestはmacOSが要るため、AndroidとiOSのテストは別の作業として書かせます。この記事ではAndroidだけを扱います。
CLAUDE.mdに書いておく規約
Claudeに毎回同じ判断をさせたくない項目は、CLAUDE.mdに固定します。次は例として使える断片です。
## モバイルE2E(Appium)
- 対象はAndroid。automationNameは UiAutomator2、クライアントは WebdriverIO
- capabilitiesのAppium独自キーには必ず `appium:` 接頭辞を付ける
- 端末はudidで指定する。deviceNameは端末選択には使われない
- ロケータの優先順位: accessibilityId、id、-android uiautomator、xpath の順
- xpathを使うときはコメントで理由を書く
- テストコードを書いたら `node --check` で構文を確認する。セッションを張る実行は
人が指示したときだけ行う
- 要素が見つからない失敗は、ページソースのXMLを読んでからロケータを直す最後の2行が、前の節で分けた線引きに対応します。Claudeが端末を触る実行は人が指示したときだけに絞り、手元で完結する確認だけは常に走らせる、という分担です。
権限は.claude/settings.jsonで、読み取り系と確認系のコマンドだけを先に許可しておくと、確認のたびに承認を待たずに済みます。次は例です。
{
"permissions": {
"allow": [
"Bash(appium driver doctor uiautomator2)",
"Bash(adb devices -l)",
"Bash(node --check:*)"
]
}
}セッションを張るテスト本体の実行(node tests/...)は、あえて許可に入れていません。端末の状態を変えるため、実行のたびに人が見える形にしておくほうが事故を防げます。ここは運用の好みで変えてかまいません。
セッション設定をClaudeに書かせる
セッション設定の書式は、Appium側のルールがはっきりしています。Claudeに渡す指示は、機種や用途の事実だけで済みます。
エミュレーター(AVD名Pixel_8_API_35)で、標準の設定アプリを起動するWebdriverIOのテストを書いて。udidはemulator-5554。capabilitiesはappium:optionsでまとめて。
ルールとして知っておく点は次のとおりです。
- Appium独自のcapabilityには
appium:接頭辞が必要です。appium:optionsにまとめれば、中では接頭辞を外せます platformNameは標準のcapabilityなので、接頭辞は付きませんappium:automationNameはuiautomator2と書き、大文字小文字は区別されませんappium:deviceNameは端末選択には使われません。実機はappium:udid、エミュレーターはappium:avdを使います- 並列実行では、
appium:udidとappium:systemPortを端末ごとに変えます - セッション開始後はcapabilitiesを変えられません。途中で変えたい挙動はSettings APIの領分です
Claudeが書く形は、たとえば次のようになります(公式のクイックスタートに沿った形です)。
const {remote} = require('webdriverio');
const capabilities = {
platformName: 'Android',
'appium:options': {
automationName: 'UiAutomator2',
udid: 'emulator-5554',
appPackage: 'com.android.settings',
appActivity: '.Settings',
printPageSourceOnFindFailure: true,
},
};
const wdOpts = {
hostname: process.env.APPIUM_HOST || 'localhost',
port: parseInt(process.env.APPIUM_PORT, 10) || 4723,
logLevel: 'info',
capabilities,
};
async function runTest() {
const driver = await remote(wdOpts);
try {
// 公式サンプルのまま xpath を使用(画面のテキストで探す例)
const appsItem = await driver.$('//*[@text="Apps"]');
await appsItem.waitForDisplayed({timeout: 10000});
await appsItem.click();
} finally {
await driver.deleteSession();
}
}
runTest().catch((e) => { console.error(e); process.exit(1); });公式のサンプルとの違いは3つです。capabilitiesをappium:optionsにまとめたこと、printPageSourceOnFindFailureを有効にしたこと、waitForDisplayedで待機を入れたことです。
printPageSourceOnFindFailureは、要素の検索が失敗したときにXMLのページソースをサーバーのログへ出す設定で、既定はfalseです。次の節で述べるように、Claudeがロケータを直すときの材料になります。
テスト用のプロジェクトには、WebdriverIOを入れておきます。
npm init
npm i --save-dev webdriverio実行はAppiumサーバーを別ターミナルで起動してから行います。
appium
node --check tests/settings-apps.js
node tests/settings-apps.jsサーバーを起動せずに実行すると、サーバーに接続できないというエラーになります。これはコードの誤りではありません。
ロケータの書き分けをClaudeに任せる
ロケータは、Claudeが最も推測で書きやすい部分です。画面の実物を見ずにボタンのテキストやIDを当てずっぽうで書くと、実行して初めて外れます。
UiAutomator2ドライバーのロケータ戦略は、公式の表で速度のランキングが付いています。
| 戦略 | 対応するUiAutomatorの仕組み | 速度の目安 |
|---|---|---|
id | 対応するUiAutomatorの仕組みリソース名の完全一致 | 速度の目安★5 |
accessibilityId | 対応するUiAutomatorの仕組みcontent descriptionの完全一致 | 速度の目安★5 |
className | 対応するUiAutomatorの仕組みクラス名の完全一致 | 速度の目安★5 |
-android uiautomator | 対応するUiAutomatorの仕組みUiSelector | 速度の目安★4 |
xpath | 対応するUiAutomatorの仕組みページソースと同じXMLツリー | 速度の目安★3 |
この順位をCLAUDE.mdの優先順位に反映したのが、前の節の規約です。accessibilityIdはReact NativeアプリではaccessibilityLabelの値に当たります。
Claudeには次の使い分けを指示すると、迷いが減ります。
- アプリ側のコードに触れるなら、まず
testIDやcontentDescriptionを付けてもらい、accessibilityIdかidで取ります - Jetpack Composeのテストタグは、リソースIDとして露出させないと
idで取れません。ドライバーのmapTestTagToResourceId設定(8.4.0以降)で、外から有効にできます - アプリの
idにパッケージ名の接頭辞が付かない場合は、disableIdLocatorAutocompletionをtrueにします - スクロールして探す動作が要るときは
-android uiautomatorのUiScrollableを使います - どうしても取れない要素に限って
xpathを使います
-android uiautomatorには注意が一つあります。GoogleがUiSelectorなどの機能をUiAutomatorから外す予定があり、そうなればこの戦略のロケータは動かなくなると、ドライバーの資料に警告があります。新規のテストではUiScrollable以外の用途に増やさないほうが後で楽です。
画面の実物をClaudeに見せる
ロケータの当たり外れを減らす最も確実な方法は、ページソースのXMLをClaudeに読ませることです。xpathは、ページソースAPIが返すものと同じXMLツリーを対象にします。つまりXMLに見えている属性が、そのままロケータの材料です。
手順は次のとおりです。
ロケータをページソースから決める流れ
- 1
エミュレーターで該当の画面を開く
テストコードの途中までを実行して、対象画面で止めます。
- 2
ページソースを保存する
WebdriverIOでは
driver.getPageSource()の戻り値をファイルに書き出します。 - 3
Claudeに読ませてロケータを決めさせる
resource-id、content-desc、textのうち、画面内で一意になる属性を選ばせます。 - 4
再実行して当たるか確かめる
外れたら、失敗時に出力されるXMLを渡して直させます。
ページソースに出てくる属性名は、resource-id、content-desc、text、classなどです。この中から、画面内で値が重複しないものを選ばせるのが基本です。
要素が見つからないときの切り分け
テストが落ちたら、Claudeには「ロケータを書き換える前に原因を分ける」ことを求めます。公式の資料も、要素が見つからない原因を段階的に挙げています。
要素が見つからないときの切り分け
- QページソースのXMLに、その要素は出ていますか?答えが出ていない答えがはい
画面遷移の途中かもしれません。待機を入れて、数秒待ってから再検索します。
- Qxpath以外のロケータを使っていますか?答えがxpathを使っている答えがはい
allowInvisibleElementsやignoreUnimportantViewsの設定を変えて試せます。XPath 2.0の解釈で問題が出る場合は、enforceXPath1をtrueにしてXPath 1.0へ戻せます。
ロケータの綴りか、対象が存在しない
xpath以外のロケータは、検索をUiAutomatorにそのまま渡します。待機後も見つからないなら、綴りの誤りか、対象が画面に無いかのどちらかです。
ここで、Claudeが勝手にxpathへ逃げないようにするのが、CLAUDE.mdの「xpathを使うときは理由を書く」の役目です。
よくあるつまずき
実行して初めて分かる失敗を、症状から並べます。
- 接頭辞の付け忘れ: Appium独自のcapabilityに
appium:が無いと、サーバーが拒否します。appium:optionsにまとめれば起きません - Appium 2とUiAutomator2の組み合わせ: ドライバー5.0.0以降はAppium 3専用です。古いAppiumに新しいドライバーは入りません
--allow-insecureの書式: Appium 3ではスコープ接頭辞が必須です。たとえば--allow-insecure=uiautomator2:adb_shellと書きます- 並列実行のポート衝突:
systemPortを端末ごとに変えないと、ポートが衝突します - テスト途中でサーバーが落ちる: テストコードがセッション削除を送ると、UiAutomator2のサーバーも終了します。以後のコマンドは接続エラーになります
この記事の線引きをまとめると
Claudeに書かせて手元で閉じられるのは、capabilitiesの書式、構文、appium:接頭辞、ドライバー要件の確認です。ロケータが画面に当たるか、機種ごとの挙動、待機の長さは、エミュレーターか実機で動かして初めて分かります。
そのため、段階1はClaudeに常時やらせ、段階2以降は人が実行を指示して、失敗時のページソースをClaudeに返す形に分けるのが運用しやすい形です。
テスト実行を完了条件にしてClaudeを止めない方法はagent hookで「テストが通るまで止めない」を検証させるに手順があります。ブラウザ側のE2Eを同じ発想で回す場合はPlaywrightのE2Eテストをhookで自動実行するが参考になります。自前の端末ではなくクラウドの実機で確認する選択肢はBrowserStack MCPでテストを計画・実行・デバッグするで扱っています。