Appiumの仕組みと使い方

# はじめに

今まで Calabash や Appium を試した記事を書いてきました。

 - [WKWebViewを使うアプリのUI自動テストにCalabash-iOSを使ってみた](http://qiita.com/k5n/items/f2ddc1f35f048fe90f3a)
 - [Appium+CucumberでiOSの自動UIテストを日本語で書く](http://qiita.com/k5n/items/6e81b5986361bd1e3cd0)

Appium を扱っていてトラブった時に、Appium の仕組みが分かっていると、原因を切り分けたりソースコードを追ったりしやすいのでまとめてみました。

詳しく解説するのは自分が利用する構成に関してのみです。ターゲットは iOS 9 で、利用言語は Ruby です。

Appium のバージョンは 1.6.0 です。

# Seleniumの構成

まずは Appium の元になっている Selenium の構成から見ていきましょう。Selenium はブラウザ上で動作する Web アプリケーションをテストするためのツールです。

![selenium.png](https://qiita-image-store.s3.amazonaws.com/0/53805/37f615a8-1b1f-1ecf-8253-2b543a2e0df9.png)

[Selenium WebDriver](http://www.seleniumhq.org/projects/webdriver/) は通信仕様を規定しています。その仕様は [W3C でドラフト状態](https://www.w3.org/TR/webdriver/) になっています。 REST Web サービスであり、つまり

 - HTTP の GET/POST/PUT/DELETE を使う
 - ステートレス
 - ディレクトリー構造に似た URI（クエリパラメータはない）
 - JSON を転送する

という仕組みになっています。 そして転送される JSON は  [JSON Wire Protocol](https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol) として定義されています。

この仕様に従った各ブラウザ用のドライバが提供されており、それらが HTTP サーバーとして動作して、クライアントと通信して対応するブラウザを操作します。

[Selenium WebDriver](http://www.seleniumhq.org/projects/webdriver/) の各ドライバのページを見ると、Selenium が提供しているドライバは Java で実装されてるっぽいです。RemoteWebDriver がすべてのドライバの親で、各ドライバはこれを継承して実装されているようです。

また [Download ページ](http://www.seleniumhq.org/download/) を見ると、Opera, Edge 用などサードパーティが提供しているドライバもあります。

WebDriver は HTTP でアクセスするサーバーですので、クライアント側は HTTP アクセスできる言語なら何でも使えます。どんな言語用のクライアントがあるかは [Download ページ](http://www.seleniumhq.org/download/) に記載されています。Selenium が用意しているもの以外にもサードパーティが用意しているものもあります。

好きな言語用のクライアントを選んだら、普通はその言語のテスティングフレームワークの中から好きなものを選んで使います。このクライアント側とドライバ側の柔軟性が Selenium の特徴であり、ブラウザテストでのデファクトスタンダードとなっています。

Selenium を最初に使った時は Selenium IDE (Firefox 拡張) に感動しました。Excel のマクロみたいに、記録ボタンを押してブラウザを操作するだけでテストコードになっていきます。アサーションも候補の中から選んで追加できます。自動 UI テストの精神的な導入コストが恐ろしく低いので、導入の説得が容易でした。

# Appiumの構成

Appium の構成を以下に図示しました。

![appium.png](https://qiita-image-store.s3.amazonaws.com/0/53805/9e50c6f1-c4a3-0f7d-6d0f-3d7b7a77f835.png)

Appium は Selenium  WebDriver の一種です。Node.js 上でサーバーとして動作し、HTTP 経由で WebDriver API を通して操作を受け付けるという仕組みです。Appium の背後には iOS 用, Android 用, Win 用などのドライバがあります。

Selenium のドライバと違って、ユーザーがドライバのどれか１つを選んで起動するわけではありません。起動するのは Appium 本体で、セッション開始時の初期化で渡す Desired Capabilities に従って、本体が対応するドライバを利用します。

Selenium WebDriver の一種と言っても、Appium で操作したいのはブラウザではなくてスマホアプリですから、スマホアプリ特有の拡張が必要です。それが [Mobile JSON Wire Protocol Specification](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md) です。

iOS 用には XCUITest, UIAutomation を使ってデバイスを操作するドライバがあります。iOS 10 からは UIAutomation が使えないため、XCUITest を使う必要があります。

Android 用には UiAutomator や Instrumentation （これは別プロジェクトだった [Seledroid](http://selendroid.io/) を使うみたい？）を使うドライバがあります。Android 4.2 以降なら UiAutomator が使えますが、それ以前なら Instrumentation を使います。

Windows 用には .Net Driver が用意されています。

# Appium

Calabash のソースコードを追いかけた後に [Appium のソースコード](https://github.com/appium/appium/tree/master/lib) を見て思ったのは、「え？これだけ？これで動くわけないでしょ？」でした。

仕組みが分かると簡単なことです。Appium 本体は単に appium コマンド実行時のコマンドラインオプションのパースとサーバーの起動処理を行っているだけです。実際にデバイスを動かす部分は各ドライバが担当しています。そして各ドライバはプロジェクトが分けてあります。[Appium のプロジェクトルート](https://github.com/appium)を辿るとドライバやクライアントのサブプロジェクトがあります。

ロギングライブラリは [appium-logger](https://github.com/appium/logger) というプロジェクトに分かれています。

[appium-base-driver](https://github.com/appium/appium-base-driver) が全てのドライバの親になります。以下の各ドライバはこれを継承する形になっています。

 - [appium-xcuitest-driver](https://github.com/appium/appium-xcuitest-driver)
 - [appium-ios-driver](https://github.com/appium/appium-ios-driver)
 - [appium-android-driver](https://github.com/appium/appium-android-driver)
 - [appium-dotnet-driver](https://github.com/appium/appium-dotnet-driver)

さらにそのドライバの下には、ドライバが利用する技術を扱うライブラリがあったりします。例えば appium-ios-driver は UIAutomation を扱う[appium-uiauto](https://github.com/appium/appium-uiauto) を利用しています。

# ライブラリ階層

iOS の場合で、Ruby を利用し、テスティングフレームワークに Cucumber を使う場合のライブラリ階層を以下に図示しました。赤色は Ruby のパッケージ、オレンジ色は Node.js のパッケージです。矢印は依存を表しています。間にある WebDriver だけはライブラリではなく仕様です。他にも関係するライブラリは沢山ありますが、主要なものだけです。

![packages.png](https://qiita-image-store.s3.amazonaws.com/0/53805/48f79a62-31e6-9def-bf26-d07faff5478b.png)

Android の場合は appium-base-driver より上は同様で、下に appium-android-driver が使われます。

Appium に所属している Node.js パッケージの依存関係は package.json を、Ruby パッケージの依存関係は gemspec ファイルを見ると、他の細かいライブラリの依存関係が分かります。

# selenium-webdriver

Selenium 公式で提供されている Ruby の WebDriver クライアントです[^capybara]。モバイル用途では後述する appium_lib がこれをラップしています[^wd]。selenium-webdriver は appium_lib をインストールすると依存関係で自動的にインストールされます。

[^capybara]: RubyではWebDriverクライアントにもなれるCapybaraが人気です。CapybaraからAppiumを利用するパッケージも存在するようです。

[^wd]: JavaScript(Node.js)版のwdではモバイル用の機能も一緒に提供されています。

Selenium のページに [Ruby Bindings](https://github.com/SeleniumHQ/selenium/wiki/Ruby-Bindings) の説明があります。また [自動生成された API ドキュメント](http://www.rubydoc.info/gems/selenium-webdriver/Selenium/WebDriver) もあります。

中心となるのは Selenuim::WebDriver::Driver と、要素を検索した時に返される Selenium::WebDriver::Element です。このうち Driver の方は appium_lib でラップされるので直接利用する機会はないかもしれませんが、Element の方は appium_lib はラップせずにそのまま返してきます。

```ruby
# ここでのdriverはSelenium::WebDriver::Driverのつもり。
# でもこれをラップしているAppium::Driverも同じメソッドを持っている。
element = driver.find_element(:id, 'Menu')
element.click
```

要素を探す時のタイムアウトを設定できます。一度設定すると設定し直すまで有効です。でもこれは appium_lib では set_wait でラップされています。

```ruby
# ここでのdriverはSelenium::WebDriver::Driver
driver.manage.timeouts.implicit_wait = 3  # 秒
```

要素が表示されるまで待つ時は以下のように書けます。でも displayed? はモバイルアプリの場合はうまく動作しないようです。

```ruby
wait = Selenium::WebDriver::Wait.new(:timeout => 3)
wait.until { driver.find_element(:id => "Menu").displayed? }
```

execute_script は渡したスクリプトを実行します。他のメソッドではできないような操作をしたい時に使えます。これも appium_lib でラップされています。以下はブラウザを操作している想定で、JavaScript を実行しています。

```ruby
element = driver.execute_script("return document.body")
driver.execute_script("return arguments[0].tagName", element)  #=> "BODY"
```

# appium_lib

Appium を Ruby から利用する際のクライアントパッケージが [appium_lib](https://github.com/appium/ruby_lib) です。これの [自動生成された API ドキュメント](http://www.rubydoc.info/github/appium/ruby_lib/Appium) もあります。Ruby を使うなら、基本的にはこれを使ってデバイスを操作することになります。

じゃあこれだけ知っていれば使い方が分かるかというとそうでもありません。find_element などで要素を選択した場合の戻り値の型は Selenium::WebDriver::Element です。つまり要素に対する click などの操作メソッドは、Selenium Web Driver 側が提供しています。

また用意されている機能だけでは実現できないことや、うまく動作しない場合のワークアラウンドなどで、直接 execute_script で JavaScript を実行したい場合があります。その場合は Appium 側の詳細を調べないと分からないかもしれません。

主な機能は Appium::Driver のインスタンスメソッドで提供されています。そしてその多くが Selenium::WebDriver へ処理を移譲するだけです。また Appium::Device, Appium::Common 両モジュールの提供するメソッドも Appium::Driver のメソッドとして扱えます[^device_common]。

[^device_common]: Appium::Driverの初期化時にそれらのモジュールをextendしている。

下記のように promote_appium_methods を使うと、"driver."を省略できるので楽です。

```ruby
driver = Appium::Driver.new(desired_caps).start_driver
# ドライバを起動した後で以下を実行すると"driver."を省略できる。
Appium.promote_appium_methods Object

driver.find_element(:id, "Menu").click
# 上記は以下のように書ける
find_element(:id, "Menu").click
```

## Driver

Driver が提供する主な機能は以下です。

起動、再起動、終了を start_driver, restart, driver_quit メソッドで行えます。

要素の取得は find_element で行います。複数要素が返る場合は find_elements を使います。これらは単に Selenium::WebDriver::Driver へ処理を委譲するだけです。

set_wait で要素取得時のタイムアウトを設定できます。

```ruby
set_wait 3  # 秒
```

exists で要素が存在するかどうかを確認できます。

```ruby
# 渡したブロック内の検索が、見つからずに例外を投げたら見つからなかったと判断する
result = exists { button('開始') }

# 存在確認前のウェイト、確認後のウェイトを指定できます。
# pre_check のデフォルト値はゼロ、post_check のデフォルト値は set_wait で指定した値です。
result = exists(pre_check: 3, post_check: 3) { button('開始') }
```

execute_script は渡したスクリプトを実行します。他のメソッドではできないような操作をしたい時に使えます。例えば iOS で UIAutomation を使っているなら、以下のように UIAutomation を直接操作する JavaScript を渡すことができます。UIAutomation の API については [UIAutomation JavaScript Reference 日本語版](https://gist.github.com/kuronekomichael/11032094) が参考になります。

```ruby
find_element(:class, 'UIATextField').click
execute_script("UIATarget.localTarget().frontMostApp().keyboard().keys()['か'].tap()")
execute_script("UIATarget.localTarget().frontMostApp().keyboard().keys()['゛'].tap()")
```

以下のようなスクリプトも実行できます。これは UIAutomation 直接ではなく、それをラップしている appium-uiauto を操作してるみたいです。

```ruby
menu = find_element(:id, 'Menu')
execute_script "au.getElement('#{menu.ref}').tap()"
```

シミュレータだと set_location で位置を設定できるみたいですが、実機では動作しないようです。

```ruby
set_location(:latitude => 34.0, :longitude => 136.0)
```

## Common

モバイルアプリのテストで便利なように WebDriver を扱うヘルパー的なメソッドが提供されています。

| メソッド | 説明 |
| :-- | :-- |
| ignore(&block) | 渡されたブロックで例外が発生しても無視します |
| back | ナビゲーションを戻ります  |
| session_id | 現在のセッションIDを返します |
| xpath(xpath_str) | find_element(:xpath, xpath_str) のエイリアス |
| get_page_class | 画面にどんなクラスが何個あるかを文字列にして返します（例: "10x UIAElement\n2x UIAWindow\n2x UIAButton"） |
| page_class | get_page_class の内容をコンソールに出力します |
| px_to_window_rel(x: 0, y: 0) | ピクセル座標をウィンドウ相対座標に変換します |

Android 関係のものやよくわからないものは省略しました。それらについてはソースコードや API ドキュメントを参照してください。

また wait_true, wait メソッドが提供されています。wait_true は渡されたブロックが真と評価されるまで待ちます。wait は例外が発生しなくなるまで待ちます。

```ruby
wait_true(timeout: 20, interval: 2, message: 'Timeout: waiting for indicator.visible == false') do
  find_element(:xpath, "//*[@name='Indicator' and @visible='false']")
end
```

## Device

Appium::Device は Appium 用の WebDriver の拡張を行います。

例えば find_element で :accessibility_id が利用できるようになっています。でもこれは普通は :id で済んでしまうので、わざわざ使わないかと思います。　

```ruby
find_element(:accessibility_id, 'Menu').click
```

以下は提供されているメソッド一覧です。

| メソッド | 説明 |
| :-- | :-- |
| available_contexts | 利用可能なコンテキストの配列を返す。WebViewに切り替える時に使う。 |
| app_strings(language = nil) | App Strings を取得する。iOS の場合 Localizable.strings を取得する？ |
| lock(duration) | デバイスを指定時間ロックする |
| install_app(path) | アプリをインストールする |
| remove_app(id) | アプリをアンインストールする |
| app_installed?(app_id) | アプリがインストールされているかどうかを返す |
| background_app(duration) | アプリを指定時間バックグラウンドにする |
| set_context(context = null) | 指定コンテキストに変更する。WebViewに切り替える時に使う。 |
| hide_keyboard(close_key = nil) | キーボードを隠す |
| press_keycode(key, metastate = nil) | 指定キーコードを押す |
| long_press_keycode(key, metastate = nil) | 指定キーコードを長押しする |
| push_file(path, filedata) | 指定パスに指定データをファイルとして配置する |
| pull_file(path) | 指定パスのファイルデータを取得する |
| get_settings | 設定を取得する |
| update_settings(settings) | 設定を更新する |
| set_network_connection(mode) | ネットワーク接続のモードを設定する |
| within_context(context) block | 指定コンテキストに切り替えて渡されたブロックを実行する。WebViewに切り替える時に使う。 |
| switch_to_default_context | デフォルトのコンテキストに切り替える。WebViewから戻す時に使う。 |

Android のみのものや、TEST ME とコメントされていたものは省略しました。それらについてはソースコードや API ドキュメントを参照してください。また実際に使ったことがないので、幾つかのメソッドはどう使うのか？何を返すのかがよく分かっていません。

また touch_actions(actions), multi_touch(actions) は次に説明する TouchAction, MultiTouch クラスが呼び出すので、直接利用することはないので省略しました。

set_network_connection で機内モードにできるかと思ったのですが、実際に実行すると 

```
$ be arc
[1] pry(main)> set_network_connection(1)
Selenium::WebDriver::Error::UnknownError: Method has not yet been implemented
```

と返ってきます。下層で実装されていないので利用できません。[Appium - Adjusting Network Connection](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/network_connection.md) によると Android では動作するみたいです。

## タッチ操作

モバイルアプリ特有のタッチ操作は Appium 側が Appium JSON Wire Protocol で TouchAction, MultiAction という API を用意しており、[Automating mobile gestures](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/touch-actions.md) というドキュメントも用意されています。これを Ruby 側で提供しているのが [Appium::TouchAction クラス](http://www.rubydoc.info/github/appium/ruby_lib/Appium/TouchAction) と [Appium::MultiTouch クラス](http://www.rubydoc.info/github/appium/ruby_lib/Appium/MultiTouch) です。

TouchAction は perform を呼び出すまでにメソッドチェーンで繋げた一連の操作を実行します。

```ruby
image = find_element(:id, 'Image')
touch = Appium::TouchAction.new
touch.tap(element: image, x: 100, y: 100, count: 2).perform
```

でもこれ、以下のようにしても長押しと認識されてしまい、うまく動作しませんでした。問題は UIAutomation にあるのかもしれませんが。

```ruby
touch
  .press(element: image, x: 100, y: 100, count: 2)
  .wait(200)
  .release
  .wait(200)
  .press(element: image, x: 100, y: 100, count: 2)
  .wait(200)
  .release
  .perform
```

MutiAction は２つ以上のタッチ操作を組み合わせるのに使うみたいです。片方の指を押さえたまま別の指をスワイプするとか。使ってないのでうまく動くのかは不明です。

# まとめ

Appium を本格的に使う際に、どうやったらやりたいことを実現できるか調べたり、トラブった時にソースコードを追いかけたりできるように、全体の仕組みと構成を説明しました。

Appium は導入コストが高いと感じているので、こういった情報がそのコストを下げる役に立てばと思っています。