Appiumを使ってiOS/Androidの要素を取得する方法の数々
Selenium/Appium Advent Calendar 2014の23番目、12月23日の記事です。少しメモしていたことがちょうどAppiumの話しだったので、ついでに。
テストエンジニアしています @Kazu_cocoaです。普段はこちらに細々ブログを書いているのですが、まとめ的なものなのでこちらに書いてみようと思います。
七転び八起きなAppiumを使ったモバイルテストのたしなみにて、Appiumを使ったモバイルアプリのテスト記述に関して、出くわしたつらみを書きました。
一方で、もっと単純な、 どうやってAppiumを実行した時にその要素を特定するか という箇所に関してまとめて言及されているような資料がないように思えたので、少しそのあたりを書いておきます。いずれも、実際に使ってみてのものなので、これ以上に使い方があるかもしれません。
環境
- Mac OS X Maverics / Yosemite
- Xcode 4.6.3, Xcode 5.1.1, Xcode 6.0, Xcode 6.1
- Appium@1.3.4以下
- ruby_lib@4.1.0
- RSpec 3 x Turnip
- Android SDK 23
iOS編
1. accessibilityLabelを使う
var accessibilityLabel: String!
@property(nonatomic, copy) NSString *accessibilityLabel
iOSが備える、Accessibilityのテキスト読み上げ機能時に読み上げられる文字列を付加するメソッドです。この要素をコード中に埋め込むことで、Appiumはaccessibility_id strategy時に要素を特定可能です。
2. accessibilityIdentifierを使う
var accessibilityIdentifier: String! { get set }
@property(nonatomic, copy) NSString *accessibilityIdentifier
UIAutomationの機構により取得可能な要素を付加するメソッドです。この要素をコード中に埋め込むことで、Appiumはaccessibility_id strategy時に要素を特定可能です。
個人的には accessibilityIdentifier
のほうがLabelよりオススメです。 accessibilityLabel
は、テキスト読み上げ機能を使った場合、ユーザが認識できる情報になります。
ユーザへの配慮を考えるのであれば、そちらはNSLocalizedStringなどを使い、ローカライズ可能な対象にすべき。一方、accessibilityIdentifier
はUIAutomator用に用意された要素の追加なので、ユーザへ配慮する必要のないところです。こちらは、保守性向上のためにローカライズなしで指定することができます。
3. XPathを使う
XPathの利用はAppium提供のInspectorを使うことで、容易に利用することができます。
find_element(xpath: '//UIAApplication[1]/UIAWindow[1]/')
ただし、特定の要素を指定できるわけではなく、要素取得時に表示されている画面に存在するパスへアクセスするだけなので以下のような用途に使えません。
- xxxの要素が表示されるまでwaitで待ち、表示されたらyyyの操作を行う
もっと柔軟に書くこともできて、
find_element(xpath: '//UIATableCell[@name="Videos"]')
のようにXPathで取得できる要素のうち、特定要素を指定することもできます。ただし、ここら辺までになるとinspectorを使ってどのUIAElementが指定されるのか、とかaccessibility_id指定よりも知見を必要とするので少し面倒です。
4. UIAElementを指定して使う
UIAutomation越しにButton要素などを取得するためのクラスとして、UIAElementというものが定義されています。こちらはAppiumではClassのstrategyを使うことで見つけることができます。
find_element(class: 'UIATextField')
5. UIAutomationをそのまま使う
UIAutomationは、JavaScriptのインタフェースを備えています。そのため、以下のようにいざとなればUIAutomationのJSをそのまま使うことができます。
execute_script("$.mainApp().alert().buttons()['#{text}'].tap();") execute_script("au.getElementByAccessibilityId('#{element}').scrollToVisible();")
6. 座標の直接指定
他にも要素の取得方法はいくつかありますが、ここらへんがよく使われるものかと思います。個人的には、accessibilityIdentifierを中心に使っています。XPathやUIAutomationの直接指定はちょっと凝ったことをしたい時に使っています。
Android編
1. contentDescriptionを使う
contentDescriptionは、音声読み上げ機能をONにしているとき、なんらかの要素タップ時に読まれる、その要素の説明文になります。Appiumではaccessiblity_idを使って要素を特定することができます。
指定方法は以下。
- Layout.xmlへ指定する
android:contentDescription=”sample”
http://developer.android.com/training/accessibility/accessible-app.html
- setContentDescriptionして指定する
- Viewを継承している要素に対しては、以下のsetContentDescriptionを使うことができます。
View.setContentDescription("sample")
これらは以下のメソッドを使うので、API Level 14以上が最低限必要になります。
一方、Appiumの要件が4.2以上を要求するので、このAPI縛りは特に問題はなさそうです。(それ以下はSelendroidを使うことになります)
2. resourceIDを使う
API Level 18から、 findAccessibilityNodeInfosByViewId
というAPIを経由することでresource idを取得できるようになったそうです。Appiumでは、以下の通りid strategyを使うことでresource idを直接指定することができるようになっています。こちらはOSでいうと4.4になります。
find_element(id: "#{package}:id/sample")
- findAccessibilityNodeInfosByViewId
Finds AccessibilityNodeInfos by the fully qualified view id's resource name where a fully qualified id is of the from "package:id/id_resource_name". For example, if the target application's package is "foo.bar" and the id resource name is "baz", the fully qualified resource id is "foo.bar:id/baz".
これにより、iOSのaccessibityIdentifierのように、ユーザに影響がない範囲でテスト自動化のための要素特定ができるようになりそうです。
3. android.widget.TextViewなどのクラスを指定する
iOSのUIAElementと同じです。
find_element(class: 'android.widget.TextView')
4. XPathを使う
以下のように続くXPathを指定することでも要素を指定できます。
FrameLayout/View/FrameLayout/View/LinearLayout....
ただし、Androidは画面に見えている要素に対してXPathを割り当てているようで、iOSに比べて動的に割り当てが変わりすぎて使いにくかったです。(使い方が間違っているかもしれませんが)
5. 座標の直接指定
6. ActionBarの要素指定
Androidアプリでは、ActionBarとして以下のクラスを利用することができます。
Action Bar | Android Developers
このクラスで使われる the [1] app icon
に該当する箇所、以下のメソッドを使いcontentDescriptionをセットする必要があります。
ActionBar | Android Developers
ただし、これはAPI Level 18以上で対応しています。それ以下のバージョンの場合、日本語では"上へ移動"というcontent descriptionが対応するようになっています。端末を英語環境にすると"up"だった気がします。
7. XPathを経由したtext要素の指定 (2015/01/04追加)
Androidネイティブアプリでは多くの場合、ユーザが触れる文字はandroid.widget.TextViewやandroid.widget.Button要素で表示されます。それらの要素に対しては、以下のXPathにより指定できます。
find_element(xpath: "//android.widget.TextView[@text=\"#{element_name}\"]") find_element(xpath: "//android.widget.Button[@text=\"#{element_name}\"]")
表示されているテキストベースで要素の取得が可能なので、一番使いやすそうです。一方、言語設定に依存する箇所なので、"1. contentDescriptionを使う"や"2. resoureIDを使う"を優先したほうが良いと思います。
===追記: 2015/02/10====
Appium@1.3.5betaくらいから、以下のようにUiSelectorを使わないと上手いこと要素を取得できませんでした。UiSelectorで特に困っては無いのでコードレベルで深く確認はしていませんが、注意したほうが良さそう。
element_name = %Q("#{element_name}") find_element :uiautomator, "new UiSelector().className(\"android.widget.TextView\").text(#{element_name});" find_element :uiautomator, "new UiSelector().className(\"android.widget.Button\").text(#{element_name});"
また、UiSelectorなら、上記のように面倒なことをせずとも、text要素の取得なら以下で済みそう。
element_name = %Q("#{element_name}") find_element :uiautomator, "new UiSelector().text(#{element_name});"
個人的には、resoirce-idを中心に使う形にしていきたいと思いますが、今は市場規模でいうと4.2もまだまだ多いので、contentDescriptionの利用が主になりそう。Androidでは、XPathは極力使わないで過ごしています。使うなら、uiautomatorが良いかなという感じ。
Appiumを操作するツール/ライブラリの好み
私はTurnipとRSpecの組み合わせを好んで使っています。
普段は、シナリオを書き、その結果としてpass/falseとスクリーンショットを取得するという位置付けのテストとしてAppiumを使っています。そのため、何をテストしているかを理解しやすくするための自然言語によるシナリオと、その実際の処理箇所が分離しているほうがテストとしての保守が容易なためです。また、シナリオと処理が分離していることで、シナリオを変更することなくOS差分を吸収するという実装もできます。それらも踏まえて、CucmberよりもTurnipのほうが好きなので、Turnip使っています。
どのライブラリを使ってAppiumを操作するかは、慣れもあると思います。一方で、テストにはテストレベルやテストタイプに応じた目的や、どのような保守性を求めるかなどの話もあります。そこらへんを総合して、自分のプロジェクトに見合ったライブラリを選ぶと良いと思います。