UIテストフレームワークCalabash-iOSを試す〜ターミナルから遠隔操作!〜

2013.05.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

前回のおさらい

前回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ができていることを確認します。確認したらこのスキーマを選択してプロジェクトをデバッグ実行してみましょう。

ios-calabash-2_1

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スキーマを選択してデバッグ実行します。

ios-calabash-2_2

これで準備完了です。あとはirbセッションを利用してターミナルからシミュレータを操作するだけです。それではどんなことができるか見てみましょう。

Query

サンプルアプリを見てみると、[Login]、[other]の2つのボタンがあるのが分かります。この状態でquery("button")を実行してみてください。

ios-calabash-2_5

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-calabash-2_3

今度はタブバーボタンをタッチしてみましょう。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>

ios-calabash-2_4

アクセシビリティ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に自動的にアクセシビリティラベルが設定されます。たとえばタブバーボタンには自動的にFirstSecondといった感じで設定されます。

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セッションを使用したシミュレータの操作を解説しました。次回からはいよいよシナリオのカスタマイズについて解説していきたいと思います!