Appium+CucumberでiOSの自動UIテストを日本語で書く（環境構築編）


# はじめに

iOS, Android 両対応の自動UIテストツールとして [Calabash](http://calaba.sh/) と [Appium](http://appium.io/) を試してみました。Calabash はテストを自然言語（つまり日本語でも）で書けるのが良いのですが、

 - テスト対象に calabash.framework を仕込むので、実際にリリースするものとは別にビルドを分ける必要がある。
 - デフォルトで用意されているコマンドでは日本語キーボードでのテキスト入力がうまくできない

という問題がありました。[その後の試行錯誤](http://qiita.com/k5n/items/ec546e08e3f960ecb106)で後者の日本語入力は何とかできるようになりましたが、この記事執筆開始時点ではまだ解決方法が分かっていませんでした。Calabash-iOS については「[WKWebViewを使うアプリのUI自動テストにCalabash-iOSを使ってみた](http://qiita.com/k5n/items/f2ddc1f35f048fe90f3a)」という記事を書いていますので参考にしてください。

Calabash は [Cucumber](https://cucumber.io/) というテストツールを iOS, Android モバイルアプリのテストに利用できるようにしたものです。つまり__日本語でテストが書けるのは Cucumber の機能__です。

一方 Appium は複数のプログラミング言語に対応しているという特徴があり、Ruby にも対応しています。じゃあ Appium の Ruby クライアントを Cucumber から利用することで、Appium のテストを日本語で書けるんじゃないかと考えました。

以下は Calabash と Appium + Cucumber の大雑把な構成を図示したものです。

![calabash_appium.png](https://qiita-image-store.s3.amazonaws.com/0/53805/e40bb165-31e7-baa2-7b3b-b2001399c299.png)

なお今や [Cucumber は Ruby 版以外も存在する](https://cucumber.io/docs) みたいです。Appium 本体は Node.js で動作するので、JavaScript 版を利用すれば Ruby 環境構築の手間が省けます。[WKWebViewを使うアプリのUI自動テストにCalabash-iOSを使ってみた（JavaScript編）](http://qiita.com/k5n/items/f5cb8932287ced1edfcc)を参照してください（結論としてはオススメでないです）。

Cucumber  から Appium を使うというアイデアは何の目新しさもないみたいです。[こんな英語の学習コース](https://www.udemy.com/test-automation-with-appium-cucumber-and-ruby/)まで用意されているくらい。しかも評価高い。この記事には ¥12,000 の価値があると思って読んでください(笑)。

関係する記事は以下です。

 - 環境構築編 （この記事）
 - [JavaScript編](http://qiita.com/k5n/items/f5cb8932287ced1edfcc)
 - [色々実験編](http://qiita.com/k5n/items/086af8fe16af2924aaa8)
 - [Appiumの仕組みと使い方](http://qiita.com/k5n/items/899cf40a0021a6a92efd)

最初に記事を作成した時の環境は El Captan, Xcode 7.3.1 利用でしたが、macOS sierra, Xcode 8.2.1にしたら、色々と動かなくなりました。特に Xcode 8 では UIAutomation が利用できないため、Appium が裏で利用するドライバを XCUITest 利用のものにする必要があります。

また XCUITest を利用するドライバは iOS 9.3 以降でないと動作しません。つまり Xcode 8 を利用する場合、iOS 9.3 未満は Appium でテストできなくなっています。

記事の内容は Xcode 8 利用想定で書き換えました。

# Node.js環境の準備

Appium を動かすための Node.js を [Homebrew](http://brew.sh/index_ja.html) [^homebrew]を使ってインストールします。Homebrew はインストール済みの想定です。

[^homebrew]: HomebrewはMacでUnixツール類をインストールするメジャーなパッケージ管理ツールです。この記事を読むような人は知っていると思うのであえてインストール方法などの説明はしません。

もし複数バージョンの node.js を管理したい場合や、指定バージョンの node.js をインストールしたい場合は、node ではなく nodebrew をインストールしてそちらで管理してください。

```
$ brew update
$ brew install node
```

# Appiumをインストール

公式の手順では -g を付けてグローバルインストールしています。それに倣います。

```
$ npm install -g appium
```

実機にアプリをインストールするのに ideviceinstaller というツールが使われます。これを Homebrew でインストールしておきます。頭に --HEAD を付けて最新ソースコードを GitHub から取ってきてコンパイル＆インストールしないと、/var/db/lockdown のパーミッションエラーで実行できません。

```
$ brew install --HEAD ideviceinstaller
```

[appium-xcuitest-driver](https://github.com/appium/appium-xcuitest-driver) の説明によると、iOS 10 では ideviceinstaller が動作しないようで、代わりに ios-deploy  を入れろと書いてあります。
 
```
$ npm install -g ios-deploy
```

また [appium-xcuitest-driver](https://github.com/appium/appium-xcuitest-driver) は、実デバイスと通信するのに [facebook/WebDriverAgent](https://github.com/facebook/WebDriverAgent) を利用します。これは appium-xcuitest-driver が必要になると勝手にビルドしてくれるのですが、その際に [Carthage](https://github.com/Carthage/Carthage) を利用しますので、インストールしておきます。

```
$ brew install carthage
```

# Ruby環境の準備

今回は Appium も Cucumber も Ruby で利用するので、まずは Ruby 自体をインストールします。

インストール方法は [WKWebViewを使うアプリのUI自動テストにCalabash-iOSを使ってみた（環境構築編） - Ruby環境の準備](http://qiita.com/k5n/items/f2ddc1f35f048fe90f3a#ruby%E7%92%B0%E5%A2%83%E3%81%AE%E6%BA%96%E5%82%99) を参考にしてください。

# AppiumのRubyクライアントをインストール

rbenv でプロジェクトで利用する Ruby バージョンを設定しておきます。プロジェクトディレクトリ直下で以下を実行します。バージョンはインストールしたものに置き換えてください。

```
$ rbenv local 2.3.1
```

[Appium の Rubyクライアント](https://github.com/appium/ruby_lib)をインストールします。ここでは Bundler を使います。Gemfile を作成して以下を記述します。

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

gem "appium_lib"
```

念のため bundle コマンドが間違ってシステムにプリインストールされているものを指していないか確認しておきます。

```
$ which bundle
/Users/username/.rbenv/shims/bundle
```

大丈夫でした。以下のコマンドを実行します。もし bundle がシステムのもの (/usr/local/bin/bundle) を指しちゃってたら rbenv exec を前に付けてください。

```
$ bundle install --path vendor/bundle
```

Nokogiri[^nokogiri] のビルド時に、依存している libxml2 のビルドに失敗してエラーになりました。途中で表示される NOTICE によると、システムにある libxml2 を使うようにもできるとのこと。でも試してもダメでした。

[^nokogiri]: HTMLやXMLから要素を抽出したりできるツールです。ネイティブコンパイルするので高速。

エラーログをよく読むと、libxml2 のビルドで LZMA_OK が undefined というエラーです。"LZMA_OK" でググると [OSX への nokogiri 1.6.8.rc3 の install でハマったメモ](http://qiita.com/south37/items/63c938474bf35b30c988) が見つかりました。これに従います。

```
$ brew install libxml2
```

libxml2 2.9.4 が入りました。この libxml2 を使うように設定してリトライ。

```
$ brew link --force libxml2
$ bundle config build.nokogiri --use-system-libraries
$ bundle install --path vendor/bundle
```

うまくいきました。

# 試しに動かしてみる

Appium は Apple が提供している UIAutomation を利用しているので、テストする端末の

設定 - デベロッパ - Enable UI Automation

を有効にしておいてください。

まずは Appium Server を起動します。停止するには Ctrl + C でいいです。

```
$ appium
```

別のコンソールを立ち上げます。

まずはデバイスの名前とUDIDを調べます。デバイスのリストが表示され、各デバイスのデバイス名、iOSバージョン, UDIDがわかります。

```
$ xcrun instruments -s devices
Known Devices:
デバイス名 (iOSバージョン) [UDID]
```

とりあえず動かしてみましょう。ファイル sample.rb に以下のテストコードを記述します。deviceName は実デバイスの名前に、udid は実デバイスの UDID に、app はアプリの Bundle ID に置き換えてください。

```ruby:sample.rb
require 'appium_lib'

desired_caps = {
  caps: {
    platformName: 'iOS',
    deviceName: 'username の iPhone',
    udid: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
    app: 'com.example.MyApp',
    automationName: 'XCUITest',
    xcodeOrgId: 'AAAAAAAAAA',
    xcodeSigningId: 'iPhone Developer'
  },
  appium_lib: {
    wait: 10
  }
}

Appium::Driver.new(desired_caps).start_driver
Appium.promote_appium_methods Object

sleep 5

button_element = find_element(:id, 'Menu')
button_element.click

sleep 5

driver_quit
```

iOS 9.3 でも XCUITest を利用するように automationName: 'XCUITest' を caps に追加しています。利用する端末が iOS 10 なら platformVersion: 10 を指定すると automationName を指定しなくても XCUITest が使われます。

また実デバイスの場合は [facebook/WebDriverAgent](https://github.com/facebook/WebDriverAgent) のビルドに必要な開発チームと署名の設定が必要です。 xcodeOrgId に開発チームID（注：「開発チーム名」ではなく10桁の「開発チームID」[^team_id]）と、xcodeSigningId に署名IDを指定しておきます（普通は'iPhone Developer'でいい）。

[^team_id]: Appleの開発者用のサイトにサインインして、Membershipのページを見ると確認できます。

ただし Appium 1.6.3 は appium-xcuitest-driver 2.4.2 をインストールするのですが、このバージョンでは xcodeOrgId, xcodeSigningId を認識してくれませんでした。[2.4.2 から 2.0.36 にダウングレードしたら動いたという報告](https://github.com/appium/appium/issues/6892)があったので、それを試しましたがうまくいかず、この時点での最新版 2.5.3 に置き換えました。

```
cd /usr/local/lib/node_modules/appium
npm uninstall appium-xcuitest-driver
npm install appium-xcuitest-driver@2.5.3
```

アプリは事前にインストールしておきます。または app の部分にアーカイブしたアプリへのパスを指定しておくと、インストールされていなければインストールしてくれます。

ここでは 'Menu' という名前（accessibilityIdentifier に 'Menu' と設定してあります）のボタンをクリックしていますが、適当に他のクリックできるものに置き換えるか、コメントアウトするかしてください。

さて実行します。Bundler でインストールした gem を使う場合は、頭に bundle exec を付ける必要があります（私は面倒なのでシェルに be で済むようにエイリアスを設定しています）。

```
$ bundle exec ruby sample.rb
```

# Cucumberをインストールする

Gemfile に [Cucumber](https://cucumber.io/) を追加します。

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

gem "appium_lib"
gem "cucumber"
```

インストールします。--path の指定は覚えていてくれるので、二回目からは指定する必要はありません。

```
$ bundle install
```

プロジェクト用のテスト環境を作成します。

```
$ bundle exec cucumber --init
```

こんなディレクトリ構造のものが出力されます。

```
features
├── step_definitions
└── support
    └── env.rb
```

# CucumberからAppiumを使ってみる

まずテストシナリオを実行する度に、実行前にアプリを起動して、実行後にアプリを終了する設定を行いたいです。Cucumber にはこのようなフック処理が書ける仕組みが用意されています。

[Cucumber Hooks](https://github.com/cucumber/cucumber/wiki/Hooks) によると、support の中にある env.rb に書けばいいんですが、別に env.rb じゃなくてもここに置いたファイルは全部認識してくれるようです。

とりあえず features/support/env.rb に以下を書いてみます。

```ruby
require 'appium_lib'

# 単に@@driverをグローバルアクセスさせたくないために用意したモジュール。
# もっと言うと単にCalabashの真似をしただけ。
module Launcher
  @@driver = nil

  def self.start
    desired_caps = {
      caps: {
        platformName: 'iOS',
        deviceName: 'username の iPhone',
        udid: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
        app: 'com.example.MyApp',
        automationName: 'XCUITest',
        xcodeOrgId: 'AAAAAAAAAA',
        xcodeSigningId: 'iPhone Developer'
      },
      appium_lib: {
        wait: 10
      }
    }
    @@driver ||= Appium::Driver.new(desired_caps)
    @@driver.start_driver  # こいつの戻り値の型はSelenium::WebDriver::Driver
  end

  def self.quit
    @@driver.driver_quit
  end

  def self.driver
    @@driver
  end
end

# Appium::Driver を appium という名前で取得できるようにする
def appium
  Launcher.driver
end

Before do |senario|
  Launcher.start
end

After do |senario|
  Launcher.quit
end
```

Appium の API には Appium::Driver 経由でアクセスします。これを appium という名前で取得できるようにしています。

次に features/sample.feature ファイルに以下を記述します。feature ファイルは Gherkin 言語で記述します。書き方は [Cucumber のフィーチャの文法 - Gherkin](http://blog.eiel.info/blog/2013/02/12/gherkin/) が参考になります。

```gherkin
# language: ja
フィーチャ: 起動

    シナリオ: 起動してメイン画面が表示される
        前提 アプリが実行中
        ならば "./screenshots/test.png"というファイル名でスクリーンショットを撮る
```

で、ステップの定義ファイルを作成します。features/step_definitions/appium_steps.rb に以下を記述します。今回は screenshot しか使ってませんが、どんな API が用意されているかは、[iOS 版で提供されている機能](https://github.com/appium/ruby_lib/blob/master/docs/ios_docs.md) を参照してください（これらが全て Appium::Driver のメソッドとして提供されているのか未確認）。

```ruby
require 'appium_lib'

Given /^アプリ(?:が|を)?実行中$/ do
  # 特にすることない
end

Then /^"([^\"]*)"というファイル名でスクリーンショットを撮る$/ do |filename|
  appium.screenshot filename
end
```

正規表現で関数を書いてるような感じです。スクリーンショットを撮るステップ定義を見てください。() で囲まれたところにマッチした文字列が do の後の引数 (filename) として渡されます。

なお (?: ) にするとマッチしたものを引数に渡しません。ここでは単に複数の書き方を許容するようにしているだけです。

 - アプリが実行中
 - アプリを実行中
 - アプリ実行中

のどれを書いても、この正規表現にマッチします。

書式は [Ruby リファレンスマニュアル - 正規表現](https://docs.ruby-lang.org/ja/latest/doc/spec=2fregexp.html) を参照してください。

スクリーンショットの出力先ディレクトリを作成しておきます。

```
$ mkdir screenshots
```

じゃあ、実行してみましょう。

```
$ bundle exec cucumber
```

うまくテストがパスしました。ちゃんとスクリーンショットも撮れています。

# まとめ

Appium + Appium Rubyクライアント + Cucumber でテストを書いて動かす環境を構築しました。あとはステップファイルを充実させて、テストを書いていくことになります。

ただしまだ WKWebView を扱えるかとか、キーボードで日本語入力ができるかとか、特定のシナリオ実行前にアプリを再インストールできるかとか、まだ試したいことがあります。それらは

[Appium+CucumberでiOSの自動UIテストを日本語で書く（色々実験編）](http://qiita.com/k5n/items/086af8fe16af2924aaa8)

に書きました。