Appiumのロケーターでもう悩まない!自動テストツールShiratesのセレクター機能
Appiumのロケーター使いこなせてますか?
オープンソースソフトウェアの自動テストツールとしてはAppiumがメジャーですが、使いこなすのはとても大変です。理由の一つに
画面上のこの要素はどうやって取得すればいいの?
というものがあり、これは永遠のテーマかもしれません。
Appiumで画面上の要素を取得するには以下のいずれかのロケータを使用することになります。
- id指定(Androidではresource-id属性、iOSではname属性が対応)
- accesibility id指定(Androidではcontent-desc属性、iOSではname属性が対応)
- class指定(Androidではclass属性、iOSではtype属性が対応)
- xpath指定(任意の属性でフィルター可能)
Appiumでは基本的にはアプリに要素取得用のidを埋め込んで、それを指定して要素を取得することが推奨されています。あなたがそのアプリの開発者なら自ら埋め込むことができますが、テスト自動化エンジニアなら開発者に依頼して埋め込んでもらう必要があります。その時点でハードルやらコストが高くなり自動化が進みにくい要因となり得るので、テスト自動化エンジニアが自ら解決できる方法を採用するのが望ましいと言えます。
idは画面上でユニークであることが前提です。それで対応ができない場合はxpathを使うことができます。xpathならアプリ開発者に依頼しなくてもテスト自動化エンジニア自ら解決できる可能性があります。ただし、一般的にはxpathは処理速度が遅いので使わないことが推奨されています。idは利用できるシーンが限定されるし, xpathは推奨しないということになると、結局どうすりゃいいの?となり、テスト自動化エンジニアにとっては要素を取得するための決定的な方法がない状況と言えます。これはツール提供者側でなんとかしてほしいですよね。
ちなみにxpathは遅いという話は常に成立するかどうかというと、そうではありません。実際に計測すると、Androidではxpathの方が速かったりします。AppiumでXPathを使用すると遅いらしいけど、どれくらい遅いの?
そもそもAppiumのよう複数のプロセスが協調しながら動作するシステムは、プロセス間で通信が発生したり、UIのアニメーション終了待ちが発生したりと、メモリ内だけで完結する処理と比較すると非常に遅いものです。次のオペレーションまで秒単位で待ち時間が発生することが普通です。メモリ内の処理でxpathが遅い部類だとしても、せいぜい100ms程度の差の話です(場合によってはxpathの方が速い)。したがって、xpathが全体としてボトルネックを発生する要因とはいえません。なので、個人的には気にせず使います。自動テストの実行時間を短縮したければ、ロケーターでチューニングするのではなく、複数プロセスを起動して並列実行した方が効果的です。
このようにAppiumのロケーターはテストを自動化するための最初の一歩である画面要素取得の手段を提供しますが、一方でテスト自動化エンジニアにとっては扱いにくいものといえます。本記事で紹介するShiratesはこの問題を独自のセレクター機能で解決を試みています。
自動テストツールShiratesのご紹介
Shirates(シラテス) はオープンソースソフトウェアで提供されているAppium互換の自動テストツールです。
使用言語はKotlinに特化していますが、テストコードの記述には専用の関数を使用するので、言語としてのKotlinのプログラミング経験がなくても使えます。いわゆるDSL(ドメイン特化言語)です。
セレクター式
Shiratesではid, accessibility id, class, xpathなどのロケーターの機能をセレクター式という記法で表現します。これはjQueryやCSSのセレクターに似た簡素な記法です。セレクター式は1つ以上のフィルター式を組み合わせて使用します。
フィルター式
| フィルター | フォーマルな書式 | 省略形 | Androidの属性 | iOSの属性 |
|---|---|---|---|---|
| text | text=text1 | text1 | text | label |
| textStartsWith | textStartsWith=text1 | text1* | text | label |
| textContains | textContains=text1 | *text1* | text | label |
| textEndsWith | textEndsWith=text1 | *text1 | text | label |
| textMatches | textMatches=^text$ | n/a | text | label |
| literal | literal=literal1 | 'literal1' | text | label |
| id | id=id1 | #id1 | resource-id | name |
| access | access=access1 | @access1 | content-desc | name |
| accessStartsWith | accessStartsWith=access1 | @access1* | content-desc | name |
| accessContains | accessContains=access1 | @*access1* | content-desc | name |
| accessEndsWith | accessEndsWith=access1 | @*access1 | content-desc | name |
| accessMatches | accessMatches=^access1$ | n/a | content-desc | name |
| value | value=value1 | n/a | text | value |
| valueStartsWith | valueStartsWith=value1 | n/a | text | value |
| valueContains | valueContains=value1 | n/a | text | value |
| valueEndsWith | valueEndsWith=value1 | n/a | text | value |
| valueMatches | valueMatches=^value1$ | n/a | text | value |
| class | class=class1 | .class1 | class | type |
| focusable | focusable=true | n/a | focusable | n/a |
| scrollable | scrollable=true | n/a | scrollable | n/a |
| visible | visible=true | n/a | n/a | visible |
| xpath | xpath=//*[@text='text1'] | n/a | (arbitrary) | (arbitrary) |
| pos | pos=2 | [2] | n/a | n/a |
| ignoreTypes | ignoreTypes=Class1,Class2 | n/a | class | type |
| image | image=image1.png | image1.png | n/a | n/a |
省略形が利用可能な場合は省略形の使用を推奨しますが、省略形で表現できない場合にはフォーマルな書式を指定できます。
セレクター機能のサンプルコード
Shiratesのセレクター機能を使用したサンプルコードを以下に示します。
select関数の引数にセレクター式を指定することで要素が取得できます。取得した要素はitプロパティに格納されます。Appiumのロケーターを使用するよりも簡素な記述でテストコードが書けます。
SelectorTest2
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import shirates.core.driver.branchextension.emulator
import shirates.core.driver.branchextension.realDevice
import shirates.core.driver.commandextension.*
import shirates.core.testcode.UITest
class SelectTest2 : UITest() {
@Test
@Order(10)
fun selectByText() {
scenario {
case(1) {
condition {
it.restartApp()
}.action {
it.select("ネットワークとインターネット")
}.expectation {
it.textIs("ネットワークとインターネット")
}
}
case(2) {
action {
it.select("ネットワークと*")
}.expectation {
it.textIs("ネットワークとインターネット")
}
}
case(3) {
action {
it.select("*とインターネット")
}.expectation {
it.textIs("ネットワークとインターネット")
}
}
case(4) {
action {
it.select("textMatches=^ネットワークとインターネット$")
}.expectation {
it.textIs("ネットワークとインターネット")
}
}
case(5) {
action {
it.selectWithScrollDown("デバイス情報||エミュレートされたデバイスについて")
}.expectation {
realDevice {
it.textIs("デバイス情報")
}.emulator {
it.textIs("エミュレートされたデバイスについて")
}
}
}
}
}
@Test
@Order(20)
fun selectById() {
scenario {
case(1) {
condition {
it.restartApp()
}.action {
it.select("#search_action_bar_title")
}.expectation {
it.textIs("設定を検索")
}
}
}
}
@Test
@Order(30)
fun selectByAccessibility() {
scenario {
case(1) {
condition {
it.restartApp()
.tap("ネットワークとインターネット")
}.action {
it.select("@ネットワークとインターネット")
}.expectation {
it.idIs("collapsing_toolbar")
}
}
}
}
@Test
@Order(40)
fun selectByClass() {
scenario {
case(1) {
condition {
it.restartApp()
}.action {
it.select(".android.widget.ImageButton")
}.expectation {
it.classIs("android.widget.ImageButton")
}
}
}
}
@Test
@Order(50)
fun selectByXpath() {
scenario {
case(1) {
condition {
it.restartApp()
}.action {
it.select("xpath=//*[@text='設定を検索']")
}.expectation {
it.textIs("設定を検索")
}
}
}
}
@Test
@Order(60)
fun selectByPos() {
scenario {
case(1) {
condition {
it.restartApp()
.tap("バッテリー")
}.action {
it.select("*バッテリー*&&[1]")
}.expectation {
it.textIs("バッテリー使用量")
}
}
case(2) {
action {
it.select("*バッテリー*&&[2]")
}.expectation {
it.textIs("バッテリーセーバー")
}
}
case(3) {
action {
it.select("*バッテリー*&&[3]")
}.expectation {
it.textIs("バッテリーを長持ちさせ、充電を最適化します")
}
}
}
}
}
サンプルコードの入手
上記のサンプルコードの完成版はこちらから入手できます。
環境構築
サンプルコードを実行する前にこちらの記事を参考にして環境構築を実施してください。
実行手順
サンプルコードの実行やテスト結果の確認方法の概要はこちらの動画を参考にしてください。※本記事とは異なるサンプルプログラムです。
相対セレクター(方向ベース)
Shiratesの相対セレクターを使用すると、取得した要素の周辺の要素を相対的に取得することができます。
基準となる要素からright, below, left, aboveを指定してウィジェットを取得することができます。

相対セレクター
| 相対セレクター | 説明 |
|---|---|
| :right | 右方向にあるウィジェット |
| :rightInput | 右方向にあるinput |
| :rightLabel | 右方向にあるlabel |
| :rightImage | 右方向にあるimage |
| :rightButton | 右方向にあるbutton |
| :rightSwitch | 右方向にあるswitch |
| :below | 下方向にあるウィジェット |
| :belowInput | 下方向にあるinput |
| :belowLabel | 下方向にあるlabel |
| :belowImage | 下方向にあるimage |
| :belowButton | 下方向にあるbutton |
| :belowSwitch | 下方向にあるswitch |
| :left | 左方向にあるウィジェット |
| :leftInput | 左方向にあるinput |
| :leftLabel | 左方向にあるlabel |
| :leftImage | 左方向にあるimage |
| :leftButton | 左方向にあるbutton |
| :leftSwitch | 左方向にあるswitch |
| :above | 上方向にあるwidget |
| :aboveInput | 上方向にあるinput |
| :aboveLabel | 上方向にあるlabel |
| :aboveImage | 上方向にあるimage |
| :aboveButton | 上方向にあるbutton |
| :aboveSwitch | 上方向にあるswitch |
方向に加えて ウィジェットタイプ(input, label, image, button, switch)を指定することができます。
ウィジェット
組み込みのウィジェットは以下のように定義されています。
| ウィジェット | 対応する型 (Android) | 対応する型 (iOS) |
|---|---|---|
| label | android.widget.TextView | XCUIElementTypeStaticText |
| input | android.widget.EditText | XCUIElementTypeTextField XCUIElementTypeSecureTextField |
| image | android.widget.ImageView | XCUIElementTypeImage |
| button | android.widget.Button android.widget.ImageButton android.widget.CheckBox |
XCUIElementTypeButton |
| switch | android.widget.Switch | XCUIElementTypeSwitch |
| widget | (上記の全て) | (上記の全て) |
セレクターコマンドの例
| 例 | 説明 |
|---|---|
<text1>:right |
text属性が"text1"である最初の要素を選択し、そこから右方向にある最初のウィジェットを選択します。 |
<text1>:right(2) |
text属性が"text1"である最初の要素を選択し、そこから右方向にある2番目のウィジェットを選択します。 |
<text1>:rightSwitch |
text属性が"text1"である最初の要素を選択し、そこから右方向にある最初のswitchを選択します。 |
<text1>:right(text2) |
text属性が"text1"である最初の要素を選択し、そこから右方向にあるtext属性が"text2"である最初のウィジェットを選択します。 |
<text1>:right:belowButton |
text属性が"text1"である最初の要素を選択し、そこから右方向にある最初のウィジェットを選択し、さらにそこから下方向にある最初のbuttonを選択します。 |
:right セレクターの使用例 (Android)
TextView1を基準とした場合

:right セレクターの使用例 (iOS)
StaticText1を基準とした場合

検索の範囲
:right セレクターは基準となる要素のtopとbottomの間を右方向に検索します。

相対セレクター機能のサンプルコード
Shiratesの相対セレクター機能を使用したサンプルコードを以下に示します。
SelectWithDirectionTest
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import shirates.core.driver.commandextension.*
import shirates.core.testcode.UITest
class SelectWithDirectionTest : UITest() {
@Test
@Order(10)
fun selectWithDirection() {
scenario {
case(1) {
condition {
it.restartApp()
}.action {
it.select("<バッテリー>:below")
}.expectation {
it.textIs("100%")
}
}
case(2) {
action {
it.select("<バッテリー>:leftImage")
}.expectation {
it.classIs("android.widget.ImageView")
}
}
case(3) {
action {
it.select("<バッテリー>:above(2)")
}.expectation {
it.textIs("通知")
}
}
case(4) {
condition {
it.tap("設定を検索")
}.action {
it.select("<@戻る>:right")
}.expectation {
it.classIs("android.widget.EditText")
}
}
}
}
}

テスト結果
HTML-Report

Spec-Report

追加情報
こちらをご参照ください。 Relative selector(Direction based)
まとめ
Appiumのロケーターは画面上の要素を取得するため手段ですが、テスト自動化エンジニアにとっては扱いにくいものとなっています。Shiratesはこの問題を独自のセレクター機能で解決を試みています。セレクター機能は簡素な記述ながら強力な検索手段を提供します。Shiratesのセレクター機能があなたをロケーターの悩みから解放してくれると確信しています。
Shiratesはオープンソースソフトウェアです。Appium互換なので、Appiumの機能を併用することも可能です。モバイルアプリのテストの自動化に興味がある方はGitHubから入手して利用してみてください。
Discussion