Appium+CucumberでiOSの自動UIテストを日本語で書く（色々実験編）


# はじめに

[環境構築編](http://qiita.com/k5n/items/6e81b5986361bd1e3cd0) の続きです。この記事は主に Appium が使えるかどうかの確認であって、Cucumber は関係ありません。

記事が長くなってしまいますが、最終的にうまくいった方法だけでなく、それまでに行ったうまくいかなかった方法も記述します。

 - 同じことにハマらないように
 - それはそれで使い方の参考になる
 - 今はうまくいかなくても、今後うまくいくようになるかもしれない

という理由からです。

利用している Appium は v1.6.0 で、iOS  9.0.2 の iPhone 6s plus 実機を用いています。

どうも Apple は iOS 10 から UIAutomation を取り除いたらしく、iOS 10 の場合は [XCUITest を使うドライバ](https://github.com/appium/appium-xcuitest-driver)を利用する必要があります。ここでは [UIAutomation を使ったドライバ](https://github.com/appium/appium-ios-driver)を利用しています。

「[WKWebViewを使うアプリのUI自動テストにCalabash-iOSを使ってみた](http://qiita.com/k5n/items/f2ddc1f35f048fe90f3a)」で試した Calabash との比較も含めて、最後にこれまで試した自動 UI テストツールについて総括します。

# Inspectorを使ってみる

Appium は GUI アプリを提供してくれています。ビューの階層が分かるだけでなく、要素に対して行った操作をコードとして記録する機能も付いています。

[Appium のページ](http://appium.io/) にある「Appium をダウンロード」をクリックします。Mac 版も Windows 版[^windows]もあるようです。ダウンロードしたら普通にインストールしてください。

[^windows]: もちろんAndroid開発用。

起動するとこんな画面が出ます。


![appium_app.png](https://qiita-image-store.s3.amazonaws.com/0/53805/210117cc-0cc9-bb4c-3b3f-88a31319e4c3.png)

まずは前の記事で desired_caps に記述した内容を設定します。ここでは前の記事と同様に iPhone 実機で動作させます。林檎マークをクリックしてください。

![appium_app_apple.png](https://qiita-image-store.s3.amazonaws.com/0/53805/bd4546b5-b24c-2026-e80a-fc1f0c4368bf.png)

アプリケーションの Bundle ID、Force Device にデバイス名、デバイスの UUID を入力します。Platform Version はデバイスの iOS バージョンを選択しておきます。もう一回林檎マークを押すとダイアログが閉じます。

Launch を押すと Appium サーバーが起動します。既にコマンドプロンプトで appium を実行している場合は終了しておいてください。なお林檎マークで設定を変更するにはサーバーは停止しておく必要があります。

サーバーが起動したら、左から４つ目の虫眼鏡アイコンを押すと Inspector が起動します。Record ボタンを押すと操作が下のコードに記録されていきます。図では Ruby を選択していますが、他の言語に切り替えられます。

![appium_app_inspector.png](https://qiita-image-store.s3.amazonaws.com/0/53805/556e9acb-0ef0-6315-742e-429b0dfe6ba5.png)

__スマホ側で操作しても記録されません__。Inspector 上で要素を選択して、Tap, Swipe などのボタンを押してコマンドを発行します。

画像を使ったボタンをタップしても何も反応がなかったり、必ずしも反応してくれるとは限らないようです。そういう場合はスマホ側で操作してから Refresh ボタンを押して画面を更新します。イマイチな出来なところもあるようですが便利だと思います。

# InspectorでWKWebView操作と日本語入力

WKWebView を使っているページを表示してみたところ、Inspector で WebView の下に UIATextFiled とか UIAStaticText とか見えています。Calabash と違い、日本語入力も完璧です。漢字で書いても入力してくれます！

```ruby
find_element(:xpath, "//UIAApplication[1]/UIAWindow[1]/UIAElement[1]/UIAScrollView[1]/UIAWebView[1]/UIATextField[1]").send_keys "名古屋駅"
```

でもボタンやスタティックテキストとして認識されているものもあるんですが、全く認識されていないものもあります。例えば jQuery Mobile でリスト表示されている部分は表示テキストは UIAStaticText として認識されていて、リンクとしては認識されていません。そしてこれをタップしても何も反応がありません。リスト項目横にアイコン画像を表示しているのですが、認識していませんね。またリンクをタップすると開くいわゆるアコーディオンも動作しません。

Calabash なら HTML の DOM を認識してくれるので、

```
touch("WKWebView xpath: '//li[.=\"表示されているテキスト\"]'")
```

とかすれば表示されているリスト項目をタップできますし、ちゃんとそれをトリガーにした JavaScript も動作します。でも Appium は UIAutomation の部品として認識しているだけで、HTML の DOM を認識してるわけじゃないみたいです。

このままだと一番の目的は WKWebView サポートなので Appium は採用できません。

# WKWebViewのDOMにアクセスする

Appium の元になっているのはブラウザテストに使われる Selenium であり、操作に使われる WebDriver のプロトコルは同じです。つまりアプリに接続するのではなく、内蔵されている WKWebView にブラウザとして接続できれば、Selenium と同じように DOM にアクセスして操作することができそうに思えるのですが・・・。

## コンテキストを切り替える

そんなことを思って調べてみると、ドキュメントに [Automating hybrid apps](https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/hybrid.md) がありました。以下のように記述してコンテキストを切り替えると DOM にアクセスできるみたいです。

Ruby 版は以下のように書くことができます。

```ruby
set_context available_contexts.last

find_element(:css, '#search').send_keys '検索文字列'
# xpath(...) は find_element(:xpath, ...) と同じ
xpath('//button[1]').click
sleep 5
xpath('//li/a[.="結果１の文字列"]').click
sleep 5
# ...

switch_to_default_context
#または
# set_context available_contexts.first
```

こんな風に書くこともできます。

```ruby
within_context(available_contexts.last) do
  find_element(:css, '#search').send_keys '検索文字列'
  xpath('//button[1]').click
  sleep 5
  xpath('//li/a[.="結果１の文字列"]').click
  sleep 5
  # ...
end
```


でもこれを試すと、以下のエラーが出ました。

```
[iOS] Attempted to get a list of webview contexts but could not connect to ios-webkit-debug-proxy. If you expect to find webviews, please ensure that the proxy is running and accessible
```

「ios-webkit-debug-proxy が動作していてアクセス可能かどうか確認しろ」と。ナニソレ？

## iOS WebKit Debug Proxyを動かす


[iOS WebKit Debug Proxy](https://github.com/google/ios-webkit-debug-proxy) は Google 様が開発しているツールみたいです。さっきの [Automating hybrid apps](https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/hybrid.md) の最後の方にも、iOS 実機で動かすのに必要と書いてありますね。iOS WebKit Debug Proxy のページだけでなく、[Appium 側にもセットアップ手順が書いてあります](https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/ios-webkit-debug-proxy.md)。

インストールします。

```
$ brew update
$ brew install ios-webkit-debug-proxy
```

起動します。今回は実機なので関係ないですが、iOS シミュレータを使う場合は先にシミュレータを起動しておく必要があるそうです。

```
$ ios_webkit_debug_proxy
Listing devices on :9221
Unable to connect to username の iPhone (xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
  Please verify that Settings > Safari > Advanced > Web Inspector = ON
```

iOS 実機側の設定で Web Inspector を ON にしないといけないらしいです。「設定 &gt; Safari &gt; 詳細 &gt; Webインスペクタ」を ON に切り替えます。Ctrl + C で終了してリトライ！

```
$ ios_webkit_debug_proxy
Listing devices on :9221
Connected :9222 to username の iPhone (xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
```

今度はうまくいきました。でもこれだとやっぱり Appium 側からの接続に失敗します。

```
[debug] [RemoteDebugger] Getting WebKitRemoteDebugger pageArray: localhost, 27753
[debug] [RemoteDebugger] Sending request to: http://localhost:27753/json
[iOS] Attempted to get a list of webview contexts but could not connect to ios-webkit-debug-proxy. If you expect to find webviews, please ensure that the proxy is running and accessible
```

うーん、ポート番号が違っていますね。ちゃんと[Appium 側のセットアップ手順](https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/ios-webkit-debug-proxy.md) を見てやりましょう。-c の引数の xxx... のところは iOS デバイスの UDID に置き換えてください。

```
$ ios_webkit_debug_proxy -c xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:27753
```

ちなみに -d をつけるとデバッグログを吐きますが、送受信パケットバイナリを HEX 表示するウザいログを表示します。ツールの開発者向けであって利用者向けのログじゃないですね。

これで Appium でコンテキストを切り替えた時のログに

```
[MJSONWP] Responding to client with driver.getContexts() result: ["NATIVE_APP","WEBVIEW_3","...
[HTTP] <-- GET /wd/hub/session/71ea51f7-e577-4806-8869-59114c3f715f/contexts 200 24 ms - 110
[HTTP] --> POST /wd/hub/session/71ea51f7-e577-4806-8869-59114c3f715f/context {"name":"WEBVIEW_3"}
[MJSONWP] Calling AppiumDriver.setContext() with args: ["WEBVIEW_3","71ea51f7-e577...
[debug] [iOS] Executing iOS command 'setContext'
[debug] [iOS] Attempting to set context to 'WEBVIEW_3'
[debug] [RemoteDebugger] WebKit debugger web socket connected to url: ws://localhost:27753/devtools/page/3
```

と出ました。成功です！

jQuery Mobile のリスト項目は、Calabash では動作した以下の XPath 指定ではタップできませんでした。

```
//li[.=\"表示されているテキスト\"]
```

以下のようにちゃんと下の a 要素を指定する必要がありました。Calabash の方がアバウトな指定でも認識してくれるみたいです。

```
//li/a[.=\"表示されているテキスト\"]
```

# ダブルタップ

[Ruby の appim_lib のドキュメント](http://www.rubydoc.info/github/appium/ruby_lib/Appium/TouchAction) には double_tap があると書いてあります。

```ruby
Then /^"([^\"]*)"をダブルタップ(?:する)?$/ do |name|
  element = find_element(:id, name)
  Appium::TouchAction.new.double_tap(element: element).perform
  sleep STEP_PAUSE
end
```

でも実際に利用するとそんなメソッドないって怒られます。

```
undefined method `double_tap' for #<Appium::TouchAction:0x007fa503747138 @actions=[]> (NoMethodError)
```

Bundler でインストールしたバージョンは 8.0.2 なんですが、これは 2016/01/29 のもので、double_tap が追加されたのは 2016/10/17 です。なので GitHub から最新を取ってくることにします。Gemfile を以下のように変更して、

```ruby
source "https://rubygems.org"

gem "appium_lib", :git => 'https://github.com/appium/ruby_lib.git'
gem "cucumber"
```

アップデートします。

```
$ bundle update
```

今度はダブルタップできましたが、ステータスバーをダブルタップしてしまい、インターネット共有設定画面に遷移してしまいました。座標指定してみます。

```ruby
Appium::TouchAction.new.double_tap(element: element, x: 200, y: 200).perform
```

これでもやはりステータスバーをダブルタップします。ステータスバーの下に隠れていない要素を指定してもダメです。なんかダブルタップ壊れてる？？[^double_tap]

[^double_tap]: さらにソースを追いかけてみると、appim_libが利用するSeleniumのRubyクライアントはtouch_double_tap(element)メソッドが座標を受け取らないんですよ。最下層のUIAutomationでもUIAElementのdoubleTapメソッドは座標を取りません。つまりappium_lib側が座標をオプションで取るのが間違いで、指定しても無視されます。でもelementも効かないのはなぜ？

要するに２回タップすればいいのだから、TouchAction を繋げてみます。こいつはメソッドチェーンで繋げた操作を perform で一気に実行してくれるみたいです。

```ruby
Appium::TouchAction.new.tap(element: element, x: 200, y: 200).wait(500).tap(element: element, x: 200, y: 200).perform
```

長押しと認識されました。tap を press + wait + release に変更してもダメです。wait(500) を wait(200) とかにしてもダメ。

もっとよく見ると、tap のオプションに :count がありますね。これを使ってみます。

```ruby
Appium::TouchAction.new.tap(element: element, x: 200, y: 200, count: 2).perform
```

これでうまくいきました。

# 要素の表示・非表示判定

exists は「存在しているかどうか」の判定です。Storyboard での配置の都合など、要素は貼り付けたままにしておいて、hidden 属性で表示・非表示だけ切り替えたいことがあります。そこで、表示・非表示を判定してみました。

```ruby
indicator =  find_element(:id, 'Indicator')
wait_true { indicator.displayed? == false }
```

この displayed? が常に false を返してきて全く役に立ちません。ググっても同じような話が出てきます。 Appim.app を実行して Inspector で見ると、visible という属性がちゃんと働いているので、これで判定できそうです。

```ruby
wait_true { indicator['visible'] == false }
```

ダメです。visible なんて属性はないというエラーになります。じゃあ何で Inspector では見えるの？

```ruby
puts get_source
```

これで Appium が管理している AST[^ast] を XML 表示してくれます。

[^ast]: 抽象構文木(Abstract Syntax Tree)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<AppiumAUT>
    <UIAApplication name="MyApp" label="MyApp" value="" dom="" enabled="true" valid="true" visible="true" hint="" path="/0" x="0" y="0" width="414" height="736">
        <UIAWindow name="" label="" value="" dom="" enabled="true" valid="true" visible="true" hint="" path="/0/0" x="0" y="0" width="414" height="736">
            <UIAActivityIndicator name="Indicator" label="進行中" value="1" dom="" enabled="true" valid="true" visible="false" hint="" path="/0/0/0" x="0" y="0" width="414" height="736">
            <!-- 略 -->
```

うーん、ちゃんと visible 属性あるじゃん。ググると [Appium, Java, iOSDriver: Can't get "visible" attribute from element](https://github.com/appium/appium/issues/6717) という情報が出てきました。Java の話ですが、結局[原因は appium-ios-driver にある](https://github.com/appium/appium-ios-driver/blob/master/lib/commands/element.js)ので Ruby でも同じ話です。

```javascript
commands.getAttribute = async function (attribute, el) {
  // ...
    if (_.includes(['label', 'name', 'value', 'values', 'hint'], attribute)) {
          let command = `au.getElement('${el}').${attribute}()`;
          return await this.uiAutoClient.sendCommand(command);
        } else {
          throw new errors.UnknownCommandError(`UIAElements don't have the attribute '${attribute}'`);
        }
  // ...
```

属性取得の際に 'label', 'name', 'value,' values', 'hint' 以外は存在しないというエラーを返すようになっています。なぜ？というか、visible 属性が取れないのもどうかと思うけど、それ以上に普通に displayed? で visible 属性の値を返すようにして欲しいんだけど・・・。

さっきの [Appium, Java, iOSDriver: Can't get "visible" attribute from element](https://github.com/appium/appium/issues/6717) で提案されている XPath を使う方法は使えそうです。

```ruby
# timeout, interval, message などのオプション引数を設定してみました
wait_true(timeout: 20, interval: 2, message: 'Timeout: indicator.visible == false') do
  xpath("//*[@name='Indicator' and @visible='false']")
end
```

Ruby なら黒魔術を使って既存クラスのメソッドを上書きできます。以下で displayed? が使えるようになりました[^isVisible]。

[^isVisible]: 多分UIAutomationのみです。XCUTestを使う場合はうまくいかないかと思います（未確認）。

```ruby
Selenium::WebDriver::Element.class_eval do
  def displayed?
    $driver.execute_script %(au.getElement('#{ref}').isVisible() === 1)
  end
end
```

WebView を使うときは元のメソッド使って欲しいので、以下のようにしておくといいでしょう。features/support/enb.rb など support 下のファイルに書いておくとテスト実行時に適用されます。　

```ruby
Selenium::WebDriver::Element.class_eval do
  alias orig_displayed? displayed?

  def displayed?
    if current_context == 'NATIVE_APP'
      $driver.execute_script %(au.getElement('#{ref}').isVisible() === 1)
    else
      orig_displayed?
    end
  end
end
```


# コンソールで実行する

個人的には Appium のイマイチ完成度の低い Inspector より Calabash コンソールの方が好みです。一般的には GUI の方がウケがいいのでしょうけど・・・。

[Appiumも少なくともRubyは一応コンソール操作するパッケージが存在する](https://github.com/appium/ruby_console)みたいなので試してみます。Gemfile に appium_console を追加します。

```
source "https://rubygems.org"

gem "appium_lib", :git => 'https://github.com/appium/ruby_lib.git'
gem "cucumber"
gem "appium_console"
```

インストールします。

```
$ bundle update
```

iOS 用の設定ファイル appium.txt を生成します。

```
$ bundle exec arc setup ios
```

いきなりエラー！

```
bundler: failed to load command: arc (/Users/developer01/Documents/dev/trial/AppiumNavista/vendor/bundle/ruby/2.3.0/bin/arc)
Errno::ENOENT: No such file or directory @ rb_sysopen - templates/appium.txt.erb
```

生成の元になるテンプレートファイルがないと。イヤイヤ、何でないの？[どうもこれっぽい](https://github.com/appium/ruby_console/blob/master/templates/appium.txt.erb)んですが、インストール時に入らなかったのかパスが違っているのか。

調べるのも面倒なので自分で appium.txt を作ります。各値はそれぞれの環境に合うように置き換えてください。

```
[caps]
platformName = "iOS"
deviceName = "username の iPhone"
udid = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
app = "com.example.MyApp"
```

コンソールを立ち上げます。REPL には irb でなく pry が使われています。

```
$ bundle exec arc
[1] pry(main)>
```

コンソール立ち上げるとまずアプリが起動します。以下で AST を XML 表示しました。

```
[1] pry(main)> puts get_source
```

find_element で検索したりも当然できます。click してメニュー画面に遷移し、ナビゲーションの戻るボタンで戻ります。

```
[2] pry(main)> element = find_element :id, 'Menu'
#<Selenium::WebDriver::Element:0x30723ac0a13ddf86 id="0">
[3] pry(main)> element.click
nil
[4] pry(main)> back
```

Ctrl-D または exit コマンドで終了します。driver_quit を実行しなくても、アプリを終了してくれます。

pry にはコマンド追加機能があります。以下をプロジェクト直下の .pryrc ファイルに記述しておくと、

```ruby
command_set = Pry::CommandSet.new do
  command "tree", "ASTをXML表示する" do
    puts get_source
  end
end
Pry.config.commands.import command_set
```

以下のように %tree で AST の XML 表示が行えるようになります。appium-console は pry のコマンドは頭に % をつけて実行します。

```
[1] pry(main)> %tree
```

ちなみに JavaScript 版クライアントの wd にも REPL モードが用意されています。

```
$ ./node_modules/.bin/wd shell
```

でも JavaScript は非同期処理なので使い勝手が悪いです。関数を実行した結果は戻り値でなくコールバック関数で受け取る仕組みなので。やっぱり Ruby がおすすめです。

# まとめ

今回は Appium でやりたいことができそうかどうか確認してみました。

今まで Calabash と Appium を試してきたので、両方を比較して総括したいと思います。

__Appium の最大の利点はアプリに何も仕込まなくてもいいこと__です。また GUI アプリが用意されていたり、複数プログラミング言語に対応していたり、日本語入力も楽だったり、__Appium の方が将来性がありそうな印象__です。Calabash は Ruby + Cucumber 固定ですが、Appium は対応言語の好きなテスティング・フレームワークを組み合わせて利用すればいいので、__Appium の方が柔軟性が高い__です。

ただし仕組みが単純だからか __Calabash の方が環境構築が楽でトラブルが少なかった__です。Appium は柔軟な分、プロジェクトも情報もあちこちに散っていて、環境構築するのに必要な情報、記述方法、トラブルの解決方法を調べるのに苦労しました。また __WKWebView を扱うのは Calabash の方が楽__です。コンテキストの切り替えが必要ないですし、iOS WebKit Debug Proxy を準備して立ち上げる必要もないですし。Cucumber から扱う場合、Appium はステップ定義をどう書けばいいのか試行錯誤する必要がありそうです。Calabash はあらかじめ英語版のステップ定義が用意されているので、__Calabash の方が初期コストがかかりません__。

__ソースコードを追いかけるのは Calabash の方が楽__です。Appiumは1.5でasync/awaitを使うように書き直され、プロジェクトも細かく分離されました。そのためソースコードはかなり読みやすくなっています。しかしそれでも

 - 下層まで追いかけようとすると、自分が使うクライアント言語の他に JavaScript を知っている必要がある
 - プロジェクトが細かく分離されていて階層が多く、全体を把握するのは結構大変

 です。

__日本語入力は Appium の方が簡単__ですが、Calabash も Appium も（iOS 9 までなら）UIAutomation を使って iOS を操作しているので、Appium がどうやっているか調べることで、Calabash も漢字指定で日本語入力できるのかもしれません。

Calabash にせよ Appium にせよ、テストを書く前に準備するのに手間がかかるし、「あれ？これできないの？どうすればいいの？」と簡単にいかないこともあって、__自動 UI テストは採用障壁が高いなぁという印象__でした。２つとも 2014 年辺りには実用レベルになっていたらしく、その頃の記事がよく見つかるし、自分が以前少し試したのもそのくらいだったのですが、２年経ってもこの程度しか成熟していないのか・・・とちょっとがっかりしました。ただでさえ自動テストはテストを書く手間の初期コストがかかるのに、加えて環境準備やステップの書き方を調べるのにこれだけ時間がかかるのでは・・・。

これまでに書いた記事が、テスト記述前の初期コストを減らす役に立ってくれることを願います。