AppiumのWebViewアプリテストの
仕組みとハマりどころ
mwakizaka(TRIDENTInc)
⾃⼰紹介
脇坂雅幸
エンジニア@TRIDENTInc.
MagicPodの開発とテニスをしています
2
MagicPod
テスト⾃動化サービス
https://magic-pod.com/
2017年07⽉ローンチ
モバイルアプリとデスクトップブラウザ
アプリのテスト⾃動化に対応
3
本⽇の話
WebViewの実体はブラウザ
AppiumによるWebViewアプリテストはChromeDevToolsのプロトコルで動いてる
WebViewアプリはWebアプリとしてテストしましょう
4
本⽇の流れ
WebView
Appium
WebViewアプリテストの仕組みとハマりどころ
Android編
iOS編
まとめ
5
WebView
6
WebView
モバイルアプリにおける画⾯要素の1つ(右図の⾚枠)
Webコンテンツを表⽰することが可能
実体はブラウザ(AndroidはChrome、iOSはSafari)
Webの技術でAndroid/iOSアプリで開発できる
さらにWebアプリとしてテスト可能
本⾴以降、WebViewを使ったアプリのことを
WebViewアプリと呼ぶことにします
7
WebViewアプリの⾃動テスト
そもそもWebView要素の中⾝の捉え⽅は2つある
i.Webな画⾯要素として捉える(e.g. input )
ii.ネイティブな画⾯要素として捉える(e.g. android.widget.EditText )
ネイティブな画⾯要素としてテストする時の問題
i.テストケース(ロケータ)が作りにくい(特にiOS)
ii.プラットフォーム差分を吸収しづらい
そのため、Webな画⾯要素としてテストする⽅が好ましい
8
WebViewアプリのデバッグAndroid(1/2)
ChromeDevToolsを使う
9
WebViewアプリのデバッグAndroid(2/2)
例えば、デスクトップブラウザアプリと同じように⾒栄えを⾊々試したりできる
10
WebViewアプリのデバッグiOS(1/2)
Safariの開発者メニューからWebInspectorを使う
11
WebViewアプリのデバッグiOS(2/2)
iOSでも同様にHTMLタグの中⾝を確認したりできる
12
Appium
13
Appium
OSSでクロスプラットフォームなE2Eテスト⾃動化フレームワーク
WebDriverを拡張したプロトコルでiOSやAndroidアプリを⾃動操作できる
WebViewアプリテストではコンテキストとウィンドウハンドルを扱う必要がある
14
コンテキスト(context)
UIコンポーネントの特定や検証の対象が「Webなのか」
「ネイティブなのか」を決めるもの
e.g.findelement時にWebViewから、Webの要素を
探すのかネイティブの要素を探すのかを決める
driver.switchContext('<コンテキスト名>'); のような
APIでコンテキストを切替える
参考:http://appium.io/docs/en/writing-running-appium/web/hybrid/index.html 15
ウィンドウハンドル(windowhandle)
Androidのみ(iOSだと普段使うことはない)
Webコンテキストの時、アプリ上のどのWebViewが
要素特定や検証の対象かを決めるもの
driver.switchToWindow('<ウィンドウハンドル名>'); の
ようなAPIでウィンドウハンドルを切替える
以下、単にウィンドウ(window)と呼ぶことにします
16
コンテキストとウィンドウのデバッグ
ChromeDevTools/WebInspectorを使う
1つのアプリに2つのWebViewが読み込まれている時、
1つのコンテキストと2つのウィンドウが存在
17
コンテキストの切り替えができない時は
ChromeDevTools/WebInspectorを使い、問題の切り分けを⾏う
WebViewのHTMLが⾒えない場合は、アプリか端末設定に問題があることが多い
⼿前味噌ですが、https://www.trident-qa.com/magic-pod-webview/の
真ん中あたりの情報が(多少)参考になると思います 18
AndroidのWebViewアプリテストの仕組み
19
AndroidのWebViewアプリテストの仕組み
AppiumはMobileJsonWireProtocolを実装したクライアントサーバモデル
SeleniumのJsonWireProtocolを拡張したもの
HTTP/REST形式で、リクエスト及びレスポンスにJSONを使う
様々なプログラミング⾔語でテストコードが書ける
ただし、ネイティブアプリをテストする場合と通信経路が少し異なる
参考:https://speakerdeck.com/jlipps/the-mobile-json-wire-protocol 20
通信経路
21
通信経路解説
以下のAppiumクライアントコード(WebDriverIO)を例に説明
1. | driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
2. | driver.$("//input[@id='datePick']") // xpathで要素を探す
22
通信経路の確⽴(1/3)
> | driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
| driver.$("//input[@id='datePick']") // xpathで要素を探す
23
通信経路の確⽴(2/3)
AppiumがChromeDriverを起動する
> | driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
| driver.$("//input[@id='datePick']") // xpathで要素を探す
24
通信経路の確⽴(3/3)
ChromeDriverでAndroid端末内のChromeDevToolsとソケット通信を確⽴する
> | driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
| driver.$("//input[@id='datePick']") // xpathで要素を探す
25
要素の探索(1/3)
AppiumクライアントからAppiumサーバにHTTPリクエストを投げる
http://localhost:<appium_port>/wd/hub/session/<session_id>/element
| driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
> | driver.$("//input[@id='datePick']") // xpathで要素を探す
26
要素の探索(2/3)
AppiumサーバからChromeDriverサーバにリクエストがプロキシされる
http://localhost:
<chrome_driver_port>/wd/hub/session/<session_id>/element
| driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
> | driver.$("//input[@id='datePick']") // xpathで要素を探す
27
要素の探索(3/3)
ChromeDriverサーバからAndroid端末のChromeDevToolsにリクエストを投げる
Runtime.evaluate {"expression": "JSコード(Selenium Atoms)",
"returnByValue": true}
ソケット通信で専⽤コマンドとJSコードを送り、ブラウザ上で実⾏している
| driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
> | driver.$("//input[@id='datePick']") // xpathで要素を探す 28
AndroidのWebViewアプリテストのハマりどころ
.ウィンドウ区別つかない問題
.ポートフィルタリングできない問題
29
1.ウィンドウ区別つかない問題
2つ以上のWebViewが読み込まれている場合、
GetWindowHandlesAPIを呼び、ウィンドウを選択する
driver.getWindowHandles();
-> ["CDwindow-05FAC41124AAF0D7A0E458BB1B689C28","CDwindow-743FE72111B559A70A90C2D08595B5E4"]
ウィンドウ名には情報量がないため、
基本的にWebコンテンツのタイトルやURLで区別する
https://appiumpro.com/editions/73-working-with-multile-webviews-in-android-hybrid-apps 30
1.ウィンドウ区別つかない問題
タイトルやURLを使っても区別がつかない場合がある
31
1.ウィンドウ区別つかない問題-原因
ウィンドウがdetachedという状態で残っていることが原因
32
1.ウィンドウ区別つかない問題-解決策
ChromeDevToolsのAPIを使う
GET /json or /json/list のAPIを使うと detached かどうかわかる
https://chromedevtools.github.io/devtools-protocol/
あまり簡単ではないので、Appiumにプルリクエストを出しました
https://github.com/appium/appium-android-driver/pull/662
Appium1.19.0から mobile:getContexts というAPIとして使えます
より詳しい話はこちら
https://blog.trident-qa.com/2020/11/android-mobile-getcontexts-api/
33
2.ポートフィルタリングできない問題
ローカルのマシンで問題なく動いたAndroidWebViewのテストが
なぜかクラウド環境で動作しなかった
SetContextAPIを呼んだ時に次のようなエラーが出た
2019-11-29 11:22:37:732 d ^[[35m[WD Proxy]^[[39m Got response with
status 200:
{"sessionId":"09c75c3f7a260230c14b48c356a4d974","status":100,"value":
{"message":"chrome not reachablen (Driver info:
chromedriver=74.0.3729.6 (255758eccf3d244491b8a1317aa76e1ce10d57e9-
refs/branch-heads/3729@{#29}),platform=Mac OS X 10.14.6 x86_64)"}}
弊社Slackから掘り起こした当時のAppiumログ。今思えばshowChromedriverLogcapabilityを使えば良かった気がする 34
2.ポートフィルタリングできない問題-原因
ChromeDevToolsのポートがポートフィルタの設定に引っかかっていた
しかもChromeDriverは任意のポート番号を使うようにハードコードされていた!
35
2.ポートフィルタリングできない問題-解決策
ChromeDevToolsのポート番号を指定するためにChromeDriverを改修
こちらはChromiumプロジェクトに修正パッチを送りました
https://chromium-review.googlesource.com/c/chromium/src/+/2433746
ChromeDriver87からcapabilityとして同様の機能が使えます
Appiumcapabilityとしての使⽤例
capabilities: {
(...),
chromeOptions: {androidDevToolsPort: <ポート番号>}
}
より詳しい(?)話はこちら
https://blog.trident-qa.com/2020/12/chromium-androiddevtoolsport/
36
iOSのWebViewアプリテストの仕組み
37
iOSのWebViewアプリテストの仕組み
iOSになっても基本は同じ。ただし、ChromeDriverは出てこない
38
通信経路
iOS実機の場合(iOSシミュレータの場合もさほど違いはありません)
39
通信経路解説
以下のAppiumクライアントコード(WebDriverIO)を例に説明
1. | driver.getContexts(); // コンテキストを取得
2. | driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
3. | driver.$("//input[@id='datePick']") // xpathで要素を探す
40
通信経路の確⽴(1/3)
> | driver.getContexts(); // コンテキストを取得
| driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
| driver.$("//input[@id='datePick']") // xpathで要素を探す
41
通信経路の確⽴(2/3)
Appiumサーバ内部でRemoteDebuggerを起動する
> | driver.getContexts(); // コンテキストを取得
| driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
| driver.$("//input[@id='datePick']") // xpathで要素を探す
42
通信経路の確⽴(3/3)
iOS端末内でWebInspectorを起動する
> | driver.getContexts(); // コンテキストを取得
| driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
| driver.$("//input[@id='datePick']") // xpathで要素を探す
43
通信経路の切替
RemoteDebuggerを使うよう、Appiumサーバの内部状態を切替える
| driver.getContexts(); // コンテキストを取得
> | driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
| driver.$("//input[@id='datePick']") // xpathで要素を探す
44
要素の探索(1/2)
AppiumクライアントからAppiumサーバにHTTPリクエストを投げる
http://localhost:<appium_port>/wd/hub/session/<session_id>/element
| driver.getContexts(); // コンテキストを取得
| driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
> | driver.$("//input[@id='datePick']") // xpathで要素を探す
45
要素の探索(2/2)
RemoteDebuggerからiOS端末のWebInspectorにリクエストを投げる
Runtime.evaluate {"expression": "JSコード(Selenium Atoms)",
"returnByValue": true}
実はAndroidと同様のプロトコルを使っている
| driver.getContexts(); // コンテキストを取得
| driver.switchContext('<WEBVIEW_***>'); // webコンテキストに切替
> | driver.$("//input[@id='datePick']") // xpathで要素を探す 46
iOSのWebViewアプリテストのハマりどころ
.Safariハング問題
.Webコンテキストが⾒つからない問題
47
1.Safariハング問題
WebViewテストのパフォーマンスが劇的に悪化
完全に固まるわけではない
再現コード(WebDriverIO)
01. | // ネイティブコンテキストで任意の要素を探す
02. | await driver.$("XCUIElementTypeWebView");
03. |
04. | // Webコンテキストを⾒つける任意のラッパー関数
05. | const webContext = await getWebContext(driver);
06. |
07. | // Webコンテキストで任意の要素を探す
08. | // 特定の条件を満たすとパフォーマンスが悪化する
09. | await driver.switchContext(webContext);
10. | await driver.$("#r106");
https://github.com/appium/appium/issues/14149 48
1.Safariハング問題
正常時のパフォーマンス
[HTTP] --> POST /wd/hub/session/1d99092f-6a81-4229-937b-c0c0ce3b9e03/context
[HTTP] {"name":"WEBVIEW_13197.1"}
[HTTP] <-- POST /wd/hub/session/1d99092f-6a81-4229-937b-c0c0ce3b9e03/context 200 610 ms - 76
[HTTP]
[HTTP] --> POST /wd/hub/session/1d99092f-6a81-4229-937b-c0c0ce3b9e03/element
[HTTP] {"using":"id","value":"r106"}
[HTTP] <-- POST /wd/hub/session/1d99092f-6a81-4229-937b-c0c0ce3b9e03/element 200 200 ms - 135
ハング時のパフォーマンス(1-2分待たされる)
[HTTP] --> POST /wd/hub/session/d1809037-a034-41c2-81e6-dace57657e7d/context
[HTTP] {"name":"WEBVIEW_11653.1"}
[HTTP] <-- POST /wd/hub/session/d1809037-a034-41c2-81e6-dace57657e7d/context 200 16075 ms - 76
[HTTP]
[HTTP] --> POST /wd/hub/session/d1809037-a034-41c2-81e6-dace57657e7d/element
[HTTP] {"using":"id","value":"r106"}
[HTTP] <-- POST /wd/hub/session/d1809037-a034-41c2-81e6-dace57657e7d/element 200 85120 ms - 135
49
1.Safariハング問題-原因
とある条件を満たすと、2⾏⽬でSafariがハングする
01. | // ネイティブコンテキストで任意の要素を探す
02. | await driver.$("XCUIElementTypeWebView");
03. |
04. | // Webコンテキストを⾒つける任意のラッパー関数
05. | const webContext = await getWebContext(driver);
06. |
07. | // Webコンテキストで任意の要素を探す
08. | // 特定の条件を満たすとパフォーマンスが悪化する
09. | await driver.switchContext(webContext);
10. | await driver.$("#r106");
50
1.Safariハング問題-再現条件
.ある程度の規模のWebコンテンツをWebViewに表⽰している
3000⾏程度のHTMLファイル?
.その画⾯でネイティブ要素をfindelementする
.iOS14.2未満(!)
iOS14.2*Appium1.19.0で再現しなくなったのを確認
51
1.Safariハング問題-解決策
今のところ、iOS14.2未満を避けるかページ(HTML)サイズを⾒直すしかない模様
MobileSafariはiOSのバージョンアップでしか更新できない
52
2.Webコンテキストが⾒つからない問題
WebViewを使っているにも関わらず、Webコンテンツが⾒えない
iOS実機 SafariのDevelopタブ
考えられる原因はいくつかあるが、最もニッチな事例を紹介
53
2.Webコンテキストが⾒つからない問題-期待値
本来は github.com - releases のような形でWebコンテンツが⾒えるはず
54
2.Webコンテキストが⾒つからない問題-原因
実はiOS実機向けのipaファイルは Development ビルドでなければならない
https://stackoverflow.com/questions/37524723/use-safari-web-inspector-with-apps-compiled-for-production/42122477#42122477 55
2.Webコンテキストが⾒つからない問題-解決策
. Development ビルドのipaファイルを使う
.ビルド済みipaファイルの署名を書き換える
fastlaneのresignAPIを使えば可能
https://docs.fastlane.tools/actions/resign/
56
まとめ
57
プラットフォーム固有な問題を乗り越えた先
WebのロケータをiOS/Androidで共有できるので、
クロスプラットフォームなテストスクリプトが作りやすくなる
テストケースの作成・メンテナンスコストを抑えられます
58
まとめ
WebViewの実体はブラウザ
AppiumによるWebViewアプリテストはChromeDevToolsのプロトコルで動いてる
WebViewアプリはWebアプリとしてテストしましょう
59
採⽤しています
テクノロジーの⼒でテストの世界を変えたいエンジニアの⽅を⼤募集中
https://www.trident-qa.com/recruit/ 60
おわり
61
以下、おまけ
62
テストケース(ロケータ)作りにくいの件(1/2)
e.g.http://example.selenium.jp/reserveApp_Renewal/で実験
Android iOS 63
テストケース(ロケータ)作りにくいの件(2/2)
対応する画⾯要素(iOSだとdatePickというidが使えない)
Androidの場合
<android.widget.EditText (...) text="2020/12/5" resource-
id="datePick" (...) />
iOSの場合
<XCUIElementTypeTextField type="XCUIElementTypeTextField"
value="2020/12/5" label="" enabled="true" visible="true" x="40"
y="315" width="140" height="40"/>
HTMLの場合
<input id="datePick" type="input" class="span4" maxlength="10"
style="font-size:22px; width:140px; text-align:center;" value="">
64
androidDevToolsPortの使い道
Android端末のDevToolsに直接アクセスすることができるようになります
const wdio = require("webdriverio");
const axios = require("axios").axios;
const option = {
(...),
capabilities: { (..), chromeOptions: {androidDevToolsPort: <CDP Port>} }
}
const driver = await wdio.remote(option);
(...)
await driver.switchContext(<contextName>); // ChromeDriverを起動する
const detailedContexts = await axios({ // CDPにリクエスト投げる
url: "http://127.0.0.1:<CDP Port>/json/list",
timeout: 2000
});
AppiumでChromeDriverを起動する必要はありますが、
指定したポート番号を使ってそれ以降は直接アクセスできます 65
1.Safariハング問題-補⾜
この問題が起きた時、以下のようなAppiumログが出ることがあるようです
アラートを出した覚えはないですが。。。
これ⾒たら、Safariハングしたかも、と思っていいかもです
[WD Proxy] Got response with status 404: {
[WD Proxy] "value" : {
[WD Proxy] "error" : "no such alert",
[WD Proxy] "message" : "An attempt was made to operate on a modal dialog when one was not open",
[WD Proxy] "traceback" : ""
[WD Proxy] },
[WD Proxy] "sessionId" : "0B4FC99E-DE0F-4E68-B7FC-C99034BA43FF"
[WD Proxy] }
[W3C] Matched W3C error code 'no such alert' to NoSuchAlertError
66
通信経路(AndroidNativeアプリ)
67
通信経路(iOSNativeアプリ)
68
Ref
iOS/AndroidのUIテストを⾃動化するAppiumのテストスクリプトの書き⽅とインスペ
クターの使い⽅(3/3)
https://www.atmarkit.co.jp/ait/articles/1506/02/news017_3.html
WebViewアプリテストのAppium公式ドキュメント
http://appium.io/docs/en/writing-running-appium/web/hybrid/
ChromeDevToolsProtocolの公式ドキュメント
https://chromedevtools.github.io/devtools-protocol/
iOSにおけるWebViewアプリのメリデメの話
https://qiita.com/susu_susu__/items/aff3b8cc26cd2d5535f8
fastlaneの使い⽅参考例
https://speakerdeck.com/kariad/apurifalsepahuomansuwoji-sok-de-niji-ce-suru?
slide=28 69
EOF
70

AppiumのWebViewアプリテストの仕組みとハマりどころ