1. Getting Started

RubyMotion では Bacon が利用できます。これは、有名な RSpec framework の小規模なクローンで、Christian Neukirchen によって開発されました。

とりわけ、RubyMotion では、iOS 向けに拡張された MacBacon と呼ばれるバージョンを使用しています。MacBacon は Eloy Duran によってメンテナンスされています。

1.1. Spec Files

Spec ファイルはプロジェクトのテストを含みます。

Spec ファイルは RubyMotion プロジェクトの spec ディレクトリ配下に作成します。

デフォルトでは、RubyMotion プロジェクトには spec/main_spec.rb ファイルがあり、このファイルにはアプリケーションが単一のウィンドウを持っていることを保証するテストが 1 つ書かれています。

1.2. Spec Helpers

たとえば Spec ファイルで使用される共通のクラスやメソッドのセットを導入というような、テストフレームワークを拡張するために、Spec ヘルパーは使用できます。Spec ファイルよりも前に、Spec ヘルパーはコンパイル・実行されます。

Spec ヘルパーは RubyMotion プロジェクトの spec/helpers ディレクトリ下に作成されます(例 : spec/helpers/extension.rb)。

デフォルトでは、RubyMotion プロジェクトに Spec ヘルパーはありません。

1.3. Running the Tests

RubyMotion プロジェクトのテストスイートを実行するために、rake タスクの spec を使用します。

$ rake spec
$ rake spec:device

このコマンドは、Spec フレームワーク、ヘルパー、Spec ファイルを含んだ特別バージョンのアプリケーションをコンパイルします。そして、シミュレータ上でバックグラウンド実行します。

いったん spec が実行されると、プログラムは適切なステータスコード (成功時には 0 、失敗時には 1) をコマンドラインのプロンプトに返します。

1.4. Run Selected Spec Files

テストスイート全体を実行するのではなく、1 つまたはいくつかの Spec ファイルのみを実行したい場合があるかもしれません。

実行すべき Spec ファイルをフィルターするために、環境変数 files にカンマ区切りの一連のパターンを設定できます。パターンは、Spec ファイルの basename (ファイルの拡張子を取り除いたもの) か、ファイルパスのいずれかになります。

例として、以下のコマンドは spec/foo_spec.rbspec/bar_spec.rb のみを実行します。

$ rake spec files=foo_spec,spec/bar_spec.rb

1.5. Output Format

環境変数 output に値を指定することで、rake spec の出力フォーマットをカスタマイズすることができます。利用可能な出力フォーマットは、spec_dox (デフォルト), fast, test_unit, tapknock です。

$ rake spec output=test_unit

2. Basic Testing

MacBacon フレームワークがサポートしている Assertion や Predicate のリストは、MacBacon の README ファイルを参照してください。

3. Views and Controllers Testing

このレイヤーでは、コントローラに対する機能テストを記述し、ハイレベルなイベント生成 API を通じてやりとりができます。みなさんは、JavaScript でテストを記述することなく、Apple の UIAutomation フレームワーク機能を活用できます。

小規模な API で構成されており、Spec や、いくつかの Run Loop ヘルパー と UIView で利用できます。

Important これは、完全なアプリケーションの受け入れテストのためのもの ではありません。したがって、アプリケーションを通常通り起動させるすべきではありません。たとえば、application:didFinishLaunchingWithOptions: から素早く return するために、RUBYMOTION_ENV を使用できます。
class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    return true if RUBYMOTION_ENV == 'test'
    # ...

3.1. Configuring your Context

Spec のコンテキストに、コントローラが指定されることを設定する必要があります。必要な API で拡張します。次のような方法で、ビューコントローラクラスを指定します。

describe "The 'taking over the world' view" do
  tests TakingOverTheWorldViewController

  # Add your specifications here.
end

各 Spec の前に、新しい window とビューコントローラクラスのインスタンスが作成されます。これらは、windowcontroller として Spec で利用できます。

Tip window または controller のカスタムインスタンスの作成を行う必要がある場合には、tests の前に呼ばれる before でそれを行うことができます。after フィルタも同様に利用できます。テストで使用するコントローラのインスタンスを参照する際に、tests メソッドを呼び出す前に after フィルタを登録する必要があります。after フィルタで window や controller のインスタンスを削除します。例えば、次のように行います。
describe "Before and after filter order illustrated" do
  # This `before` filter is defined __before__ the one of the `tests` method, so at this point the
  # window instance is not yet created and you can perform controller initialization.  It is
  # important to note, though, that once you call `window` it will be created on-demand.
  before do
    controller.dataThatNeedsToBeConfiguredBeforeAssigningToWindow = ['TableView Row 1', 'TableView Row 2']
  end

  # This `after` filter is defined __before__ the one of the `tests` method, so at this point the
  # window and controller instances are not yet cleaned up and set to `nil`.
  after do
    controller.performCustomPostTestWork
    window.performCustomPostTestWork
  end

  tests TakingOverTheWorldViewController
  # Calling `tests` registers a `before` and an `after` filter that do something like the following:
  #
  #  before do
  #    createWindowAndControllerIfNotCreatedYet
  #  end
  #
  #  after do
  #    cleanUpWindowAndController
  #  end

  it "performs tests" do
    # ...
  end
end

3.1.1. Storyboards

tests メソッドに :id オプションでコントローラの Xcode identifier を渡すことで、storyboard 8976コントローラをテストできます。

tests StoryboardViewController, :id => 'controller-id'

デフォルトでは、プロジェクトの resources ディレクトリにある MainStoryboard.storyboard ファイルからコントローラがロードされます。:storyboard オプションで storyboard のファイル名を渡すことで、異なるファイルからコントローラをロードすることができます。

tests StoryboardViewController, :storyboard => 'AlternateStoryboard', :id => 'controller-id'
Tip :id オプションに対応する Identifier フィールドは、Xcode で View ControllerAttributes Inspector で確認できます。Attributes Inspectorcommand-option-4 というキーボードショートカットで表示できます。

3.2. Durations

いくつかのメソッドは、秒単位でイベントが生成される期間を指定するために :duration というオプションがあります。:duration常にオプションです。

Tip Bacon::Functional.default_duration= で、デフォルトの継続時間の値を変更できます。

3.3. Device Events

以降のメソッドはデバイスレベルで影響を与えるイベントを生成します。それらのメソッドは、アクセシビリティラベルや特定のビューを取りません。

3.3.1. rotate_device

指定された方向にデバイスを回転させます。

rotate_device(:to => orientation, :button => location)
  • to: デバイスを回転させる方向。:portrait:landscape のいずれかを指定します。

  • button: portrait/landscape の向きを示すために使用します。:portrait モードでは :bottom:top のいずれか、:landscape モードでは :left:right のいずれかを指定します。省略した場合には、:portrait モードでは :bottom:landscape モードでは :left がデフォルトとなります。

以下の例では、デバイスの左側にホームボタンが来るように、横方向へデバイスを回転します。

rotate_device :to => :landscape

あるいは、デバイスの右側にホームボタンが来なくてはいけない場合には、以下のようになります。

rotate_device :to => :landscape, :button => :right

3.3.2. accelerate

加速度のイベントを生成します。

accelerate(:x => x_axis_acceleration, :y => y_axis_acceleration,
           :z => z_axis_acceleration, :duration => duration)
  • x: デバイスを縦方向で画面を正面に保持した状態で、x 軸はデバイスの左側 (マイナス) から右側 (プラス) の方向にあります。

  • y: デバイスを縦方向で画面を正面に保持した状態で、y 軸はデバイスの底部 (マイナス) から上部 (プラス) の方向にあります。

  • z: デバイスを縦方向で画面を正面に保持した状態で、y 軸はデバイスの背面 (マイナス) から正面 (プラス) の方向にあります。

背面方向にデバイスを振る動作をシミュレートするには、次のようになります。

accelerate :x => 0, :y => 0, :z => -1

3.3.3. shake

加速度のイベントを生成します。

shake()

特に上下左右に揺さぶる (Shaking-Motion) イベントを引き起こしたい場合に、このメソッドを使用します。

詳しくは event handling guide を参照ください。

3.4. Finding Views

ビューを取得するためのメソッドをこのセクションで扱います。これらのメソッドは、現在の window からビュー階層を下層へ走査します。

一致するビューがない場合、timeout で指定された間はリトライし続けます。timeout のデフォルト値は 3 秒です。探しているビューがまだロードされているか、あるいはアニメーションしているかどうか心配する必要はないでしょう。

タイムアウトが発生し、一致するビューがない場合には例外が発生します。

Tip デフォルトのタイムアウト値は Bacon::Functional.default_timeout= で変更できます。

3.4.1. view

指定されたアクセシビリティラベルと一致するビューを返します。

view(label)

例:

button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
button.setTitle('Take over the world', forState:UIControlStateNormal)
window.addSubview(button)

view('Take over the world') # => button
Tip UIView#viewByName(accessibilityLabel, timeout) を参照ください (https://github.com/HipByte/RubyMotion/blob/0928fafe6fffba2a99043bc66725d3b582aa99ae/lib/motion/spec/helpers/ui.rb#L123 で定義されています)。

3.4.2. views

指定されたクラスと一致するすべてのビューの配列を返します。

views(view_class)

例:

button1 = UIButton.buttonWithType(UIButtonTypeRoundedRect)
button1.setTitle('Take over the world', forState:UIControlStateNormal)
window.addSubview(button1)

button2 = UIButton.buttonWithType(UIButtonTypeRoundedRect)
button2.setTitle('But not tonight', forState:UIControlStateNormal)
window.addSubview(button2)

views(UIButton) # => [button1, button2]
Tip UIView#viewsByClass(viewClass, timeout) を参照ください (https://github.com/HipByte/RubyMotion/blob/0928fafe6fffba2a99043bc66725d3b582aa99ae/lib/motion/spec/helpers/ui.rb#L127 で定義されています)。

3.5. View Events

このセクションで扱うメソッドはすべてビューを操作します。操作するビューのアクセシビリティラベルか、ビューのインスタンスを渡すことでビューを指定します。

Note 一般的に、すべての UIKit コントローラはアクセシビリティラベルの適切なデフォルト値を持っています。タイトルが“Take over the world”のボタンは、アクセシビリティラベルに同じ値を持っています。しかしながら、カスタムビューを利用する場合や、デフォルト値を上書きする必要がある場合には、accessibilityLabel 属性を設定することによってそれを行うことができます。

location を必要とされる場合には、CGPoint のインスタンスか以下の定数のいずれかを指定できます。

  • :top_left

  • :top

  • :top_right

  • :right

  • :bottom_right

  • :bottom

  • :bottom_left

  • :left

Note CGPoint のインスタンスは window 座標で指定されなければなりません。
Tip いくつかのメソッドは、:from:to を location オプションで取ります。上記の location 定数で :from:to のみを指定した場合にはほかのオプションは省略でき、指定した location の反対側がデフォルトで設定されます。 CGPoint のインスタンスを使用する場合には、ほかのオプションも同様に指定しなければなりません。

3.5.1. tap

ビューのタップをシミュレートするイベントを生成します。

tap(label_or_view, :at => location, :times => number_of_taps, :touches => number_of_fingers)

以下のオプションはすべて省略可能です。

  • at: タップを発生させる location を指定します。デフォルトはビューの中央が設定されます。

  • times: ビューをタップする回数を指定します。デフォルトはシングルタップが設定されます。

  • touches: ビューをタップするのに使用する指の数を指定します。デフォルトではシングルタッチが設定されます。

ビューを 1 回タップする場合は次のようになります。

button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
button.setTitle('Take over the world', forState:UIControlStateNormal)
window.addSubview(button)

tap 'Take over the world'

ビューを 2 回 2 本の指でタップする場合は、次のようにオプションを指定します。

view = UIView.alloc.initWithFrame(CGRectMake(0, 0, 100, 100))
view.accessibilityLabel = 'tappable view'
recognizer = UITapGestureRecognizer.alloc.initWithTarget(self, action:'handleTap:')
recognizer.numberOfTapsRequired = 2
recognizer.numberOfTouchesRequired = 2
view.addGestureRecognizer(recognizer)

tap 'tappable view', :times => 2, :touches => 2

3.5.2. flick

フリック操作のイベントを生成します。

flick(label_or_view, :from => location, :to => location, :duration => duration)
  • from: ドラッグを開始する location を指定します。

  • to: ドラッグが終了する location を指定します。

スイッチをフリックする場合は、次のようになります。

switch = UISwitch.alloc.initWithFrame(CGRectMake(0, 0, 100, 100))
switch.accessibilityLabel = 'Enable rainbow theme'
window.addSubview(switch)

flick 'Enable rainbow theme', :to => :right

3.5.3. pinch_open

ピンチアウト操作のイベントを生成します。

pinch_open(label_or_view, :from => location, :to => location, :duration => duration)
  • from: 両方の指がジェスチャーを開始する location を指定します。デフォルトでは :left が設定されます。

  • to: 移動する方の指のジェスチャーが終わる location を指定します。デフォルトでは :right が設定されます。

以下は、UIScrollView のコンテンツを拡大表示します。

view('Zooming scrollview').zoomScale # => 1.0
pinch_open 'Zooming scrollview'
view('Zooming scrollview').zoomScale # => 2.0

3.5.4. pinch_close

ピンチイン操作のイベントを生成します。

pinch_close(label_or_view, :from => location, :to => location, :duration => duration)
  • from: 移動する方の指のジェスチャーを開始する location を指定します。デフォルトでは :right が設定されます。

  • to: 両方の指がジェスチャーを終了する location を指定します。デフォルトでは :left が設定されます。

以下は、UIScrollView のコンテンツを縮小表示します。

view('Zooming scrollview').zoomScale # => 1.0
pinch_close 'Zooming scrollview'
view('Zooming scrollview').zoomScale # => 0.5

3.5.5. drag

指定された開始・終了位置の間をドラッグする操作のイベントを生成します。

drag(label_or_view, :from => location, :to => location, :number_of_points => steps,
     :points => path, :touches => number_of_fingers, :duration => duration)
  • from: ドラッグを開始する location を指定します。:points が指定された場合には、使われません。

  • to: ドラッグを終了する location を指定します。:points が指定された場合には、使われません。

  • number_of_points: :from から :to を補間するパスのポイント数を指定します。デフォルトでは 20 が設定されます。:points が指定された場合には、使われません。

  • points: ドラッグするパスを指定する CGPoint インスタンスの配列を指定します。

  • touches: ドラッグに使用する指の数を指定します。デフォルトではシングルタッチが設定されます。

Note ドラッグした方向とは逆方向にスクロールすることに注意してください。

以下は、スクロールビューを下へスクロールします。

view('Scrollable scrollview').contentOffset.y # => 0
drag 'Scrollable scrollview', :from => :bottom
view('Scrollable scrollview').contentOffset.y # => 400

3.5.6. rotate

ビュー中央を中心に時計回りに回転操作するイベントを生成します。

rotate(label_or_view, :radians => angle, :degrees => angle, :touches => number_of_fingers,
       :duration => duration)
  • radians: ラジアン単位の回転角度を指定します。デフォルトでは π が設定されます。

  • degrees: 度単位の回転角度を指定します。デフォルトでは 180 が設定されます。

  • touches: 回転に使用する指の数を指定します。デフォルトでは 2 が設定されます。