UIテストフレームワークCalabash-iOSを試す〜ターミナルから遠隔操作!〜
前回のおさらい
前回はCalabash-iOSの導入方法について解説しました。今回からはGithubで公開されている01 Getting started guide · calabash/calabash-ios Wiki · GitHubを参考に、Calabash-iOSの使い方を解説していきたいと思います。
今回は公式ドキュメントにあるように、既に用意されているサンプルプロジェクトを使用して実際にCalabash-iOSを導入し、実行中のサンプルアプリに対してターミナルから対話的にコマンドを実行する方法を解説したいと思います。シナリオを早く書きたいところですが、何事も基本が大事!ということでまだ我慢してください。
サンプルプロジェクトを作成する
まずは今回の解説に使用するサンプルプロジェクトを作成します。サンプルプロジェクトは既にGithubに公開されていますのでそちらをcloneするなりダウンロードするなりしましょう。
calabash/calabash-ios-example
サンプルプロジェクトにCalabash-iOSを設定する
Calabash-iOSのセットアップ
サンプルプロジェクトLPSimpleExampleにはまだCalabash-iOSが設定されていませんので設定しましょう。
作成したサンプルプロジェクトのディレクトリ(LPSimpleExample.xcodeprojがあるところ)に移動します。
$ cd path-to-calabash-ios-example
calabash-ios setupコマンドを実行して、サンプルプロジェクトにCalabash-iOSを設定します。尚、Xcodeを起動していると失敗するので終了しておきましょう。
$ calabash-ios setup
設定が完了したら、サンプルプロジェクトLPSimpleExampleをXcodeで起動し、スキーマにLPSimpleExample-calができていることを確認します。確認したらこのスキーマを選択してプロジェクトをデバッグ実行してみましょう。
Xcodeのコンソールに以下のように表示されればOKです。
2013-05-27 10:26:43.559 LPSimpleExample-cal[1207:c07] Creating the server: <LPHTTPServer: 0x6e38db0> 2013-05-27 10:26:43.561 LPSimpleExample-cal[1207:c07] Calabash iOS server version: 0.9.144 2013-05-27 10:26:43.562 LPSimpleExample-cal[1207:c07] simroot: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator5.0.sdk 2013-05-27 10:26:43.569 LPSimpleExample-cal[1207:c07] Started LPHTTP server on port 37265 2013-05-27 10:26:44.270 LPSimpleExample-cal[1207:c07] BECOMEACTIVE 2013-05-27 10:26:44.290 LPSimpleExample-cal[1207:1e03] Bonjour Service Published: domain(local.) type(_http._tcp.) name(Calabash Server)
スケルトンを生成する
ターミナルでcalabash-ios genコマンドを実行してスケルトンを生成します。
$ calabash-ios gen ----------Question---------- I'm about to create a subdirectory called features. features will contain all your calabash tests. Please hit return to confirm that's what you want. --------------------------- <RETURN> ----------Info---------- features subdirectory created. Try executing cucumber ---------------------------
プロジェクトディレクトリを確認し、以下のようにファイル・ディレクトリが作成されていればOKです。
- calabash-ios-example/
- features/
- my_first.feature
- step_definitions/
- support/
テストを実行できるか確認する
calabash-ios genコマンドで生成されたサンプルのシナリオを実行してみます。いったんデバッグ実行を終了して、cucumberコマンドを実行してみましょう。
$ cucumber Feature: Running a test As an iOS developer I want to have a sample feature file So I can begin testing quickly Scenario: Example steps # features/my_first.feature:6 ------------------------------------- Auto detected APP_BUNDLE_PATH: APP_BUNDLE_PATH=/Users/hoge/Library/Developer/Xcode/DerivedData/LPSimpleExample-askmoeoeyhmbdjejlljrfzqtqsxv/Build/Products/Debug-iphonesimulator/LPSimpleExample-cal.app Please verify! If this is wrong please set it as APP_BUNDLE_PATH in features/support/01_launch.rb ------------------------------------- Waiting for App to be ready Given I am on the Welcome Screen # features/step_definitions/my_first_steps.rb:1 Then I swipe left # calabash-cucumber-0.9.144/features/step_definitions/calabash_steps.rb:211 And I wait until I don't see "Please swipe left" # calabash-cucumber-0.9.144/features/step_definitions/calabash_steps.rb:149 And take picture # calabash-cucumber-0.9.144/features/step_definitions/calabash_steps.rb:206 1 scenario (1 passed) 4 steps (4 passed) 0m10.704s
テストがうまくいけば、iOSシミュレータが起動してサンプルアプリが実行されます。テストが終わると自動的に終了するので、終了したらプロジェクトディレクトリを見てみましょう。 すると、screenshot_0.pngという名前でサンプルアプリのスクリーンショットが保存されていると思います。
エラーが発生した場合、XCodeで生成するビルドファイルの配置場所が想定と違う場所になっている可能性が高いらしいです。Calabash-iOSが想定するビルドファイルの配置場所は、
~/Library/Developer/Xcode/DerivedData/[プロジェクト名]-abcdefg/Build/Products/Debug-iphonesimulator/[プロジェクト名]-cal.app
となっています。別の場所にビルドファイルを作成している場合は、環境変数APP_BUNDLE_PATHにそのパスを設定しましょう。
テスト環境のカスタマイズ
Calabash-iOSでは以下の環境変数が用意されています。
環境変数 | 使用例 | 説明 |
---|---|---|
SDK_VERSION | SDK_VERSION=5.1 | iOS SDKのバージョンを指定してシミュレータを起動します。指定がない場合はデフォルトのシミュレータが起動されます。 |
NO_LAUNCH | NO_LAUNCH=1 | シミュレータを起動させないようにします。主に実機でテストする場合やLessPainfulを使用したテスト行う場合に使用します。 |
DEVICE | DEVICE=iphone(またはipad) | 実機でのテスト用のエンドポイントを指定します(詳細は07 Testing on physical iDevicesを参照)。 |
OS | OS=ios5(その他、ios4、ios6が指定可能) | シミュレータのiOSのバージョンを指定します。 |
RESET_BETWEEN_SCENARIOS | RESET_BETWEEN_SCENARIOS=1 | シナリオ実行毎にシミュレータをリセットします。 |
これらの環境変数はcucumberコマンドのオプションとして指定できます。
例
$ cucumber RESET_BETWEEN_SCENARIOS=1 NO_LAUNCH=0 SDK_VERSION=6.0 DEVICE=iphone
my_first.featureを見てみよう
calabash-ios genコマンドで生成されたmy_first.featureを見てみましょう。
features/my_first.feature
Feature: Running a test As an iOS developer I want to have a sample feature file So I can begin testing quickly Scenario: Example steps Given I am on the Welcome Screen Then I swipe left And I wait until I don't see "Please swipe left" And take picture
このファイルにはサンプルとしてとても簡単なシナリオが書かれています。実は私もシナリオの書き方についてはまだよくわかっていませんが(この辺の書き方は勉強したら次回あたりで書きたいと思います)、ざっと説明するとFeatureにはこのシナリオの目的を記述し、Scenarioにはシナリオと実行するステップが書かれています。そしてここで定義されているシナリオはたぶん「画面が表示されたら、左へスワイプし、スクリーンショットを撮る」的な感じだと思います。なんかまだよくわかりませんw
まぁ、とりあえずサンプルに従って、「画面が表示されたら、タブバーの2番目のボタンをタッチする」という感じに書き換えてみましょう。
features/my_first.feature
Feature: Running a test As an iOS developer I want to have a sample feature file So I can begin testing quickly Scenario: Example steps Given I am on the Welcome Screen And I touch "Second"
これを実行してみるとこのシナリオに書いた通り画面表示後、一瞬だけ2番目のタブバーボタンに紐づく画面が表示されます。なるほど。
ちなみに定義済みのステップとステップのカスタマイズについては以下のURLで詳しく解説されているらしいです。この辺の詳しい説明次回以降にまわして、公式ドキュメントに従い次に進みます。
ターミナルからシミュレータを操作する
ここからはターミナルからコマンドを使用してしてシミュレータを操作する方法を解説して行きます。
早速ターミナルでcalabash-ios consoleを実行してみましょう。
$ calabash-ios console Running irb... irb(main):001:0>
次にXcodeを開き、LPSimpleExample-calスキーマを選択してデバッグ実行します。
これで準備完了です。あとはirbセッションを利用してターミナルからシミュレータを操作するだけです。それではどんなことができるか見てみましょう。
Query
サンプルアプリを見てみると、[Login]、[other]の2つのボタンがあるのが分かります。この状態でquery("button")を実行してみてください。
irb(main):001:0> query("button") [ [0] { "class" => "UIRoundedRectButton", "rect" => { "center_x" => 136, "y" => 307, "width" => 72, "x" => 100, "center_y" => 325.5, "height" => 37 }, "frame" => { "y" => 287, "width" => 72, "x" => 100, "height" => 37 }, "UIType" => "UIControl", "description" => "<UIRoundedRectButton: 0x84b1f50; frame = (100 287; 72 37); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x84b2020>>" }, [1] { "class" => "UIRoundedRectButton", "rect" => { "center_x" => 145.5, "y" => 235, "width" => 73, "x" => 109, "center_y" => 253.5, "height" => 37 }, "frame" => { "y" => 215, "width" => 73, "x" => 109, "height" => 37 }, "UIType" => "UIControl", "description" => "<UIRoundedRectButton: 0x84a5190; frame = (109 215; 73 37); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x84a52e0>>" } ] irb(main):006:0>
2つのボタンの情報が配列として表示されていることが分かります。また、以下のようにすることでこの情報をさらに深く探ることができます。
irb(main):006:0> result = query("button") [ [0] { "class" => "UIRoundedRectButton", "rect" => { "center_x" => 136, "y" => 307, "width" => 72, "x" => 100, "center_y" => 325.5, "height" => 37 }, "frame" => { "y" => 287, "width" => 72, "x" => 100, "height" => 37 }, "UIType" => "UIControl", "description" => "<UIRoundedRectButton: 0x84b1f50; frame = (100 287; 72 37); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x84b2020>>" }, [1] { "class" => "UIRoundedRectButton", "rect" => { "center_x" => 145.5, "y" => 235, "width" => 73, "x" => 109, "center_y" => 253.5, "height" => 37 }, "frame" => { "y" => 215, "width" => 73, "x" => 109, "height" => 37 }, "UIType" => "UIControl", "description" => "<UIRoundedRectButton: 0x84a5190; frame = (109 215; 73 37); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x84a52e0>>" } ] irb(main):007:0> btn1 = result[0] { "class" => "UIRoundedRectButton", "rect" => { "center_x" => 136, "y" => 307, "width" => 72, "x" => 100, "center_y" => 325.5, "height" => 37 }, "frame" => { "y" => 287, "width" => 72, "x" => 100, "height" => 37 }, "UIType" => "UIControl", "description" => "<UIRoundedRectButton: 0x84b1f50; frame = (100 287; 72 37); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x84b2020>>" } irb(main):008:0> class1 = btn1["class"] "UIRoundedRectButton" irb(main):009:0>
query関数は文字列を引数として受け取ります。この引数にはCSSセレクタのように記述することができます。例えば、「最初に見つかったボタンの中のすべてのラベルを見つける」場合は以下のようにします。
irb(main):009:0> query("button index:0 label") [ [0] { "class" => "UIButtonLabel", "rect" => { "center_x" => 136, "y" => 316, "width" => 38, "x" => 117, "center_y" => 325.5, "height" => 19 }, "frame" => { "y" => 9, "width" => 38, "x" => 17, "height" => 19 }, "UIType" => "UIView", "description" => "<UIButtonLabel: 0x84b2250; frame = (17 9; 38 19); text = 'other'; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x84b2340>>" } ] irb(main):010:0>
query関数は非常に強力で、様々な方法で要素にアクセスできます。詳しくは05 Query syntaxに記載されているのでそちらを見てみてください。余力があればこの辺の内容も次回以降で触れてみたいと思います。
Touch
query関数で取得できる要素であれば、touch関数を用いて触れることができます。iOSのシミュレータを見ながらtouch("button index:0")を実行してみてください。これは「最初に出現するボタン、すなわち[other]ボタンをタッチする」という意味になります。これを実行すると、[other]ボタンがタッチされ一瞬青くなるはずです。
irb(main):011:0> touch("button index:0") [ [0] "<UIRoundedRectButton: 0x84b1f50; frame = (100 287; 72 37); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x84b2020>>" ] irb(main):012:0>
今度はタブバーボタンをタッチしてみましょう。iOSのシミュレータを見ながらtouch("tabBarButton index:1")を実行してみてください。これは「インデックス1のタブバーボタン、すなわちSecondタブをタッチする」という意味になります。これを実行すると、タブバーボタンに紐づく画面(Second Viewと書かれた地図の画面)表示されるはずです。
irb(main):012:0> touch("tabBarButton index:1") [ [0] "<UITabBarButton: 0x84a0570; frame = (82 1; 76 48); opaque = NO; layer = <CALayer: 0x84a0250>>" ] irb(main):013:0>
アクセシビリティIDとラベルの設定
今まではquery("button")やtouch("tabBarButton index:1")など抽象的な指定の仕方で要素を特定していましたが、一般的なやり方としてはUIViewのアクセシビリティID、またはアクセシビリティラベルを用いて要素を特定します。サンプルアプリのFirstと書かれているタブバーボタンをタップして画面を元に戻し、query("view marked:'switch'")を実行してみましょう。
irb(main):013:0> query("view marked:'switch'") [ [0] { "class" => "UISwitch", "rect" => { "center_x" => 145.5, "y" => 168, "width" => 79, "x" => 106, "center_y" => 181.5, "height" => 27 }, "frame" => { "y" => 148, "width" => 79, "x" => 106, "height" => 27 }, "UIType" => "UIControl", "description" => "<UISwitch: 0x84ad760; frame = (106 148; 79 27); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x84ad830>>" } ] irb(main):014:0>
このように記述すると、アクセシビリティID、またはアクセシビリティラベルにswitchが設定されている要素が検索されます。
通常であれば、ほとんどのUIViewに自動的にアクセシビリティラベルが設定されます。たとえばタブバーボタンには自動的にFirst、Secondといった感じで設定されます。
irb(main):014:0> touch("tabBarButton marked:'Second'") [ [0] "<UITabBarButton: 0x84a0570; frame = (82 1; 76 48); opaque = NO; layer = <CALayer: 0x84a0250>>" ] irb(main):015:0>
アクセシビリティラベルの指定や使用を明示的に制御する場合は、viewDidLoadメソッドあたりに以下のように記述することで行えます。
-(void) viewDidLoad { [super viewDidLoad]; self.uiswitch.isAccessibilityElement = YES; self.uiswitch.accessibilityLabel = @"switch"; self.button.isAccessibilityElement = YES; self.button.accessibilityLabel=@"login"; }
accessibilityIdentifier
これまでのサンプルではaccessibilityLabelを使用する方法を紹介しましたが、通常accessibilityLabelはローカライズされてしまいます。そのため、accessibilityIdentifierを使用した方がいいですのですが、困ったことにaccessibilityIdentifierはiOS5以降から追加されたものなので、状況に応じて使い分けましょう。
次回
今回はサンプルプロジェクトを使用してシナリオの簡単なカスタマイズから、irbセッションを使用したシミュレータの操作を解説しました。次回からはいよいよシナリオのカスタマイズについて解説していきたいと思います!