WKWebViewを使うアプリのUI自動テストにCalabash-iOSを使ってみた（環境構築編）

# はじめに

[Calabash](http://calaba.sh/) は iOS, Android 両対応の UI テストツールです。Xamarin[^xamarin] が管理しているようです。

[^xamarin]: XamarinはiOS/Androidアプリも作れるクロスプラットフォームの.Net開発環境で、2016年2月にMicrosoftに買収されました。

試した時のバージョンは 0.20.3 です。Mac の OS は El Captan で、Xcode 7.3.1 利用です。

以下のシリーズになっています。

 -  環境構築編（この記事）
 - [日本語準備編](http://qiita.com/k5n/items/06d8d1b1056f23a2fe1e)
 - [色々実験編](http://qiita.com/k5n/items/ec546e08e3f960ecb106)

# 特徴

他によく使われる iOS, Android 両対応のUIテストツールに [Appium](http://appium.io/) があります。Appium と比較した場合の [Calabash-iOS](https://github.com/calabash/calabash-ios) の特徴は以下です。

 - テストシーケンスを自然言語で（つまり日本語でも）書ける
 - アプリに calabash.framework を仕込む必要があり、リリース用とテスト用でビルドを分ける必要がある

一方 Appium の特徴は以下です。

 - Java, C#, Javascript, Python, Ruby, PHP, Perl といった多くの言語に対応している
 - Selenium が元になっているので、Selenium 経験者なら学習コストが低い
 - アプリに何も仕込む必要がない

Calabash の（というか Calabash が使う Cucumber の）最大の特徴はテストの記述方法だと思います。これならエンジニアでなくても何やってるか分かりますね。

```gherkin
# language: ja
フィーチャ: 初期化
    アプリを起動すると初期化を行う。
 
    シナリオ: 初期画面を表示する
        前提 アプリを起動する
        ならば "初期化中"と表示されていること
        ならば スクリーンショットを撮る
        もし 初期化完了を待つ
        ならば "初期化中"と表示されていないこと
        かつ "メニュー"ボタンが表示されていること
        ならば スクリーンショットを撮る
```

Calabash には文字入力に少々難があったため（解決方法は[色々実験編](http://qiita.com/k5n/items/ec546e08e3f960ecb106)に記載）、Calabash を試しつつ Appium + Appium Rubyクライアント + Cucumber という構成も試して「[Appium+CucumberでiOSの自動UIテストを日本語で書く](http://qiita.com/k5n/items/6e81b5986361bd1e3cd0)」という記事にしました。

# 採用検討の背景

今回のアプリは機能の一部が Web サービスとして実装されていて、WKWebView を使っています。Xcode 標準の UI テストツールではごく一部のテストしかできない状況でした。WKWebView サポートが Calabash を検討した最大の理由だったんですが、Appium も 1.5.3 で WKWebView をサポートしたようです[^appium]。

[^appium]: 最初の検討段階ではAppiumではWKWebViewを扱えないという報告が散見された。

開発チームのリーダーはプログラマではないため、UIテストはできればプログラマでなくても読める書式が望ましいです。

Calabash の最大のデメリットは、実際にリリースするものとは別にテスト用ビルドを用意しないといけないことです。今回のアプリの品質要件は厳しくありません。またテスト環境のサーバーを使ってアプリ単体の網羅的なテスト（Calabash 利用）を行った後に、サーバー環境も含めたシステム全体のテスト（手動）を行うという手順になっています。Calabash を外すと大きな問題が出る場合は、その後のシステムテストで検知できるんじゃないかと考えています。

ちなみに筆者は Selenium は利用したことがあり、Calabash-Android も少し試用したことがあります。どちらも心理的障壁は低いです。

# Ruby環境の準備

[Mac デフォルトの Ruby を使うのは推奨されていません](https://github.com/calabash/calabash-ios/wiki/Ruby-on-MacOS)。Calabash を使うための Ruby 環境を用意してくれる [Calabash Sandbox](https://github.com/calabash/install) が便利そうなのですが、これはメンテが大変なんで out of date だとこっそり書いてあります（が、まだマニュアルの至る所で使う想定になっています）。

Ruby には指定バージョンの Ruby をインストールしたり、複数バージョンを切り替えられるマネジメントツールが用意されています。[rbenv](https://github.com/rbenv/rbenv) と [rvm](https://rvm.io/) がありますが、推奨されている rbenv を使います。

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

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

```
$ brew update
$ brew install rbenv
```

以下を実行します。

```
$ rbenv init
```

すると、自分の環境では zsh を使っているため以下が表示されました。

```
# Load rbenv automatically by appending
# the following to ~/.zshrc:

eval "$(rbenv init -)"
```

言われた通り、~/.zshrc の最後に eval "$(rbenv init -)" を追記します。コマンドプロンプトを立ち上げ直すか、source .zshrc して追記したものを有効にしておきます。

以下でインストール可能な Ruby バージョンのリストが表示されます。

```
$ rbenv install --list
```

最新安定版の 2.3.1 を入れることにします。

```
$ rbenv install 2.3.1
```

インストールされるまで待ちます。以下のコマンドでインストール済みのバージョンが表示されます。Mac デフォルトの system 以外に 2.3.1 が表示されるはずです。

```
$ rbenv versions
```

今いるディレクトリでだけ 2.3.1 を使うように設定します。

```
$ rbenv local 2.3.1
```

全体で利用するバージョンを指定する場合は、 local でなく global を指定します。以下で現在使っているバージョンを表示できます（注: versions ではなく version）。

```
$ rbenv version
$ ruby --version
```

どっちもちゃんと 2.3.1 になってれば OK ！

[Calabash の説明 ](https://github.com/calabash/calabash-ios/wiki/Ruby-on-MacOS)によると、~/.gemrc に以下の設定をすると、ドキュメントがインストールされないので、通信量が減ってパッケージのインストールが早く終わるそうです。

```
install: --no-document --env-shebang
update:  --no-document --env-shebang
```

Ruby には RubyGems というパッケージ管理の仕組みがあり、パッケージやツールを gem として管理します。gem コマンドで普通にインストールすると、全てのプロジェクトで同じものを利用することになります。Calabash は Bundler という各プロジェクトで利用するパッケージを別々に管理するツールの利用を勧めています。

```
$ gem install bundler
Fetching: bundler-1.13.6.gem (100%)
Successfully installed bundler-1.13.6
1 gem installed
```

Bundler が入りました。バージョンを確認してみます。

```
$ bundler --version
Bundler version 1.13.1
```

えええーーーーバージョン違ってるじゃん！

```
$ which bundle
/usr/local/bin/bundle
```

ああ、これは何でか知らんけどシステムプリインストールの Ruby の bundle を指してるわ。こういう時は、

```
$ rbenv exec bundler --version
Bundler version 1.13.6
```

って感じで rbenv exec を付けて rbenv 管理の Ruby で bundle を実行すると、ちゃんとインストールしたものを利用してくれます。ちなみに自分の環境では後述するインストール作業を終えた後にふと確認してみると、なぜか rbenv exec 付けなくても良くなってました。なぜ？？

# テスト用ビルドの方法

３種類の方法が用意されています。

 1. Debugビルド時のみ calabash.framework をリンク
 2. Debug, Release とは別にテスト用のコンフィグレーションを用意
 3. 別のターゲットを用意

1 だと calabash.framework を仕込む以外にも、リリースコンパイルされないなど、リリース版との違いが大きくなります。論理動作が確認できれば十分なのであれば、この方法で、しかもシミュレーターでテストでもいいかもしれません。

2 は Release, Debug とは別にテスト用のコンフィグレーションを追加します。これなら calabash.framework を仕込む以外は Release と一緒にできます。デバッグ中は Debug を使い、UI自動テストにはこちらを使い、リリース時には Release を使います。

3 はターゲットを複製するわけですが、複製後にファイルを追加するときに両方のターゲットをチェックする必要があるなど、管理が面倒です。

今回は 2 を選択しました。

# テスト用コンフィグレーションの準備

[チュートリアル](https://github.com/calabash/calabash-ios/wiki/Tutorial:-Calabash-config) の通りに作業するだけです。やり方が変わっているかもしれないので、この記事は日本語の参考情報程度と思って[チュートリアル](https://github.com/calabash/calabash-ios/wiki/Tutorial:-Calabash-config)に従ってください。

## コンフィグレーションを追加する

![create_calabash_configuration.png](https://qiita-image-store.s3.amazonaws.com/0/53805/1df9dddc-72f0-3c9b-ab9f-6bc3f251fa3c.png)

Release 用と同じ設定にしたかったので、ここで Duplicate "__Release__" Configuration を選択しました。名前はチュートリアル通り "Calabash" [^configuration] にしました。

[^configuration]: "Test"も考えたのですがちょっと違うかなと。こいつは「Calabashによる自動テスト用」であり、手動テストはReleaseでやるつもりなので。

## calabash.frameworkをダウンロードする

calabash.framework をダウンロードします。.xcodeproj と同じディレクトリに、以下の内容の Gemfile を用意します。これは Bundler が利用するファイルです。

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

gem "calabash-cucumber", ">= 0.16", "< 2.0"
```

で、以下のコマンドを実行します。bundle がシステムのものを指しちゃってる場合は rbenv exec を前に付けてください。

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

ここでは --path で gem のインストール先を指定しています。なぜチュートリアルでは指定していないのか不明です[^path]。

[^path]: pathを指定しないとホームディレクトリ下にインストールされるので、プロジェクトごとに別々でなくなります。それなら何のためにBundler使うのか？グローバル設定でvendor/bundleに入れるようになっている想定なのかもしれません。

自分の環境ではこの時点で rbenv exec が必要なくなってました。続けて calabash.framework をダウンロードします。

```
$ bundle exec calabash-ios download
```


Bundler を使うとコマンド実行の度に bundle exec をつけることになります。[Calabash のBunlder のススメ](https://github.com/calabash/calabash-ios/wiki/Best-Practice:-Use-Gemfile-and-bundler) ではシェルにエイリアスを定義して be で済むように設定しています。

プロジェクトルートにダウンロードされた calabash.framework を他の場所に格納したい場合は移動しておきます。

```
$ mv calabash.framework Frameworks/
```

## calabash.frameworkをリンクする

calabash.framework へのリンク設定を行います。Linking - Other Linker Flags の Calabash コンフィグレーションのところだけ以下を設定します。Other Linker Flags をアプリのターゲットで上書きしてると、プロジェクト設定に書いても反映されませんので注意です。

```
-ObjC -force_load "$(SOURCE_ROOT)/calabash.framework/calabash" -framework CFNetwork
```

calabash.framework を別の場所に移動していたらパスを修正します。以下は Framworks ディレクトリに入れた場合。また既に CFNetwork をリンクしている場合は "-framework CFNetwork" は要りません。

```
-ObjC -force_load "$(SOURCE_ROOT)/Frameworks/calabash.framework/calabash"
```

![other_linker_flags.png](https://qiita-image-store.s3.amazonaws.com/0/53805/1919c109-7311-9116-1aea-049d2b487ba0.png)

それ以外にも Release と違うものは変更しておきます（コード署名を変更しました[^codesign]）。

[^codesign]: ReleaseはArichiveしてアップロードするためのDistribution設定にしていたので。

## スキーマを追加する

最後に Calabash コンフィグレーションでビルド、実行するスキーマを作成します。画面左上の停止ボタン右側にあるスキーマ選択部分をクリックし、Manage Schemes... を選択します。

![calabash_scheme.png](https://qiita-image-store.s3.amazonaws.com/0/53805/4a7fcf77-36c3-f60f-d191-44cd98dd04a4.png)

（テスト用でなく）アプリ用のスキーマを選択して、ギアマークから Duplicate を選択して複製します。

![duplicate_scheme.png](https://qiita-image-store.s3.amazonaws.com/0/53805/829be63a-30c2-3c44-dacc-f19844f0bdc9.png)

Shared にチェックを入れておきます。

![scheme_shared.png](https://qiita-image-store.s3.amazonaws.com/0/53805/cf4814f6-7a0a-eed3-45f4-9ee29f02b2e1.png)

Edit... を押して、Build Configuration に Calabash を設定します。

![build_configuration.png](https://qiita-image-store.s3.amazonaws.com/0/53805/9a3bc033-aefd-af75-9cac-bf4f0c973186.png)

このスキーマで実機かシミュレーターで実行すると、以下のような Calabash server のログが出力されるはずってことですが・・・

```
DEBUG CalabashServer:222 | Creating the server: <LPHTTPServer: 0x7f80b3d066e0>
DEBUG CalabashServer:223 | Calabash iOS server version: CALABASH VERSION: 0.16.4
```

実機で動作させて実際に出力されたログはこんな感じでした。おお！なんか WKWebView を使えそうな雰囲気です！

```
2016-10-21 14:55:33.422 DEBUG CalabashServer:252 | Creating the server: <LPHTTPServer: 0x13dd64130>
2016-10-21 14:55:33.423 DEBUG CalabashServer:253 | Calabash iOS server version: CALABASH VERSION: 0.20.3
2016-10-21 14:55:33.423 DEBUG CalabashServer:256 | App Base SDK: iphoneos9.3
2016-10-21 14:55:33.423 DEBUG CalabashServer:288 | IPHONE_SIMULATOR_ROOT: (null)
2016-10-21 14:55:33.426 DEBUG CalabashServer:274 | Calabash iOS server is listening on: (null) port 37265
2016-10-21 14:55:33.729 DEBUG LPWKWebViewRuntimeLoader:221 | WKWebView successfully implemented LPWebViewProtocol
```

# Calabashテスト作成の準備

ここからは [Calabash-iOS の REAME](https://github.com/calabash/calabash-ios) の手順に従って作業します。

以下のコマンドを実行すると features ディレクトリを作成し、その中にテストの雛形を出力してくれます。

```
$ bundle exec calabash-ios gen
```

以下のコマンドでとりあえず雛形を実行してみましょう。

```
$ bundle exec cucumber
```

calabash server をリンクしたアプリが見つからないと言って怒られます。え？リンクしたの実行したじゃん！・・・どうもこれはシミュレータで実行しようとするらしく、先ほど実行したのは実機だったから、シミュレータ用はビルドされていなくて見つからないと。勝手にビルドはしてくれないみたいなので、ビルド＆テストしたい場合はそのためのスクリプトを作る必要がありそうです。

なお見つからない場合は以下のように APP 環境変数で教えてやれと書いてありますが、実機用のビルド結果を指定しても、実機で動作してくれるわけではないようです。下記の &lt;&gt; で囲まれた部分は適宜置き換えてください。シミュレータで動かす場合は iphoneos は iphonesimulator に置き換えます・・・が、普通はこれを設定しなくても見つけてくれるはず。

```
$ export APP="~/Library/Developer/Xcode/DerivedData/<UDID>/Build/Products/Calabash-iphonesos/<NAME>.app"
```

# 実機でテストを実行する

実機でテストする方法は [Testing on Physical Devices](https://github.com/calabash/calabash-ios/wiki/Testing-on-Physical-Devices) に書いてあります。

まず実機が開発可能な設定になっていて Mac を USB で繋ぐのは当たり前として、__同じネットワークに繋いでおく必要がある__そうです。これは同じ WiFi ルータに繋いでもいいですし、iPhone のテザリング（USB テザリングでも OK）を使って Mac を iPhone に接続してもいいですし、逆に Mac のインターネット共有機能を使って Wifi アクセスポイントにして、iOS デバイスを Mac に接続してもいいです。

また

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

を有効にします。

Calabash Android と違って Calabash iOS にはアプリをインストールする機能はないので手動でインストールします。まぁこれは普通に Xcode 上から Run すればいいですね。

実機で動作させるためには以下の３つの情報が必要とのこと。

 - デバイスの UDID
 - デバイスの IP アドレス
 - アプリのバンドル ID

## デバイスのUDID

Xcode の Window - Devices を選択してデバイスマネージャを起動します。左側でデバイスを選択すると、identifier というところに UDID が表示されます。

コマンドラインからなら以下でデバイス一覧が表示され、各デバイスの UDID が分かります。

```
$ xcrun instruments -s devices
````

または以下でも実機の UDID が表示されます。

```
$ bundle exec calabash-ios console
calabash-ios 0.20.3> RunLoop::Instruments.new.physical_devices
```

calabash のコンソールは Ctrl+D で抜けます。ちなみにこのコンソール上からデバイスを操作したり、ビュー階層を表示したりできるので、使い方を覚えると便利です。

## デバイスのIPアドレス

デバイスを WiFi に繋いだ場合は、デバイスの 設定 - Wi-Fi の画面上で接続しているネットワークの i アイコンをタップすれば表示されます。

iPhone のテザリングを使って Mac を iPhone に繋いだ場合は、Mac 側で

```
$ ifconfig 
```

を実行するとネットワーク一覧が表示されます。lo0 は localhost なので無視して、inet に IP アドレスが表示されているものを探します。自分の環境では以下の 172.20.10.3 が iPhone に繋いだ __Mac 側の__ IP アドレスです。iPhone 側の IP アドレスは一番下の桁を 1 に変えた 172.20.10.1 になります。

```
en4: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
        （略）
        inet 172.20.10.3 netmask 0xfffffff0 broadcast 172.20.10.15
        （略）
```

アプリを手動で実機で実行します。以下のコマンドを実行します。 172.20.10.1 の所は自分の実機の IP アドレスに置き換えてください。

```
$ curl http://172.20.10.1:37265/version
```

以下のように表示されれば Calabash と通信できています。

```
{"device_family":"iPhone","outcome":"SUCCESS","server_port":37265,（以下略）
```

ちなみに Apple が提供している Bonjour という機能があり、iPhone はそのサーバー機能を持っているので、IP アドレスの部分をデバイス名に変えても見つかるらしい・・・んですが、デバイス名が空白を含まず URL でサポートされている文字だけで出来てないとダメ。

## アプリのバンドルID

Xcode でアプリのターゲットを選択し、General 画面の Identity - Bundle Identifier に表示されているものです。"com.example.MyApp" みたいな形式のやつ。

## 動かしてみる

取得した３つの情報を環境変数に設定します。

```
$ export BUNDLE_ID=com.example.MyApp
$ export DEVICE_TARGET=< UDID >
$ export DEVICE_ENDPOINT=http://<ip>:37265
```

テストを実行してみましょう。

```
$ bundle exec cucumber
Feature: Sample Feature

  Scenario: Sample Scenario          # features/sample.feature:3
    Given the app has launched       # features/steps/sample_steps.rb:1
    And I have done a specific thing # features/steps/sample_steps.rb:7
    When I do something              # features/steps/sample_steps.rb:32
    Then something should happen     # features/steps/sample_steps.rb:41

1 scenario (1 passed)
4 steps (4 passed)
0m12.801s
```

おお！アプリが起動して無事テストが完了しました。

環境変数の設定は、いつも同じならファイルに保存して実行すると楽です。ここでは setup_iphone6splus_for_calabash.sh というファイルに保存したとします。子シェルで export した内容は親シェルに反映されないので、実行するのではなく source します。

```
$ source setup_iphone6splus_for_calabash.sh
```

# 次へ

[WKWebViewを使うアプリのUI自動テストにCalabash-iOSを使ってみた（日本語準備編）](http://qiita.com/k5n/items/06d8d1b1056f23a2fe1e) に続きます。