Google+ もご覧ください
ユーザーアイコン

実践!RubyMotion

第六回 RubyMotionでコントローラ/ビューのテストを書こう

Satococoa

第六回 RubyMotionでコントローラ/ビューのテストを書こう

前回はモデルのユニットテストを書いてみました。今回はビューやコントローラのテストをしてみましょう。

テストの準備と RUBYMOTION_ENV

まず最初に、ビューやコントローラをテストできるこの機能ですが、受け入れテストのためのものではありませんので注意してください。

そのため 公式のドキュメント (和訳) でも、テスト時には即座に通常のアプリケーションの起動プロセスをストップさせるよう、以下のように書くことを推奨しています。

app/app_delegate.rb に追加。

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    return true if RUBYMOTION_ENV == 'test'  # この一行を追加
    # (以下略)

ここで出てきた RUBYMOTION_ENV という定数ですが、"test""development""release" のいずれかの値を持つようになっています。

通常は "development" という値なのですが、rake spec として実行した場合は "test" になり、rake archive:distribution で Apple への提出用のビルドを行った場合には "release" になっています。

テストの正確性と実行速度を上げる目的から、テスト実行時にはアプリの起動時に UIWindow のインスタンスを作ったりするという処理を全てスキップし、不要なオブジェクトを作らないように推奨されているのだと思います。

app/app_delegate.rb に上記のコードを追加したら rake spec と打ってみましょう。既存のテストが一つ失敗します。

(スクリーンショットでは、失敗が分かりやすいように output=colorized オプションをつけています。)

main_spec.rb が失敗
main_spec.rb が失敗

Application 'RMQiita'
  - has one window [FAILED - 0.==(1) failed]

Qiita::Client

.fetch_tagged_items

API へのアクセスに成功するとき
  - 取得したJSONをもとにオブジェクトが作られる

API からエラーが返されるとき
  - エラーメッセージが取得できる

インターネットに接続できないとき
  - エラーメッセージが取得できる

Qiita::Item
  - 属性が正しいこと

Bacon::Error: 0.==(1) failed
  spec.rb:698:in `satisfy:': Application 'RMQiita' - has one window
  spec.rb:712:in `method_missing:'
  spec.rb:314:in `block in run_spec_block'
  spec.rb:438:in `execute_block'
  spec.rb:314:in `run_spec_block'
  spec.rb:329:in `run'

5 specifications (19 requirements), 1 failures, 0 errors

先ほど追加したコードによって、テスト実行時に通常通りにアプリが起動しなくなったためですね。

この main_spec.rb はテストの書き方の例示以外に特に目的はないと思いますので、削除してしまいましょう。

ビュー・コントローラのテスト

EntriesController のテスト

では早速 EntriesController に対するテストを書いてみます。

テストの内容ですが、Qiita API から返されるデータを tableView に正しく表示できることの確認とします。

Qiita API は前回導入した WebStub を使ってスタブ化します。

spec/controllers/entriescontrollerspec.rb (controllers ディレクトリは作成してください。)

describe "EntriesController" do
  extend WebStub::SpecHelpers

  before do
    @data = [
      {
        'title' => 'title1',
        'user' => {'url_name' => 'name1'},
        'updated_at_in_words' => '1 days ago',
        'body' => '<p>body1</p>'
      },
      {
        'title' => 'title2',
        'user' => {'url_name' => 'name2'},
        'updated_at_in_words' => '2 days ago',
        'body' => '<p>body2</p>'
      },
    ]
    stub_request(:get, "https://qiita.com/api/v1/tags/RubyMotion/items").
      to_return(json: @data)
  end

  tests EntriesController # 1. EntriesController をインスタンス化

  it '取得したデータが表示されること' do
    wait 0.2 do # データを取得し、テーブルビューに描画されるのを待つ
      controller.tableView.numberOfRowsInSection(0).should.equal @data.count
      view('title1').should.not.be.nil
      view('title2').should.not.be.nil
    end
  end

end

tests EntriesControllerEntriesController クラスのオブジェクトが生成され、 controller という変数でアクセスできるようになります。

また、view('title1')title1 という文字列をアクセシビリティラベルにもつビューのオブジェクトを取得しています。

ではさっそく実行してみます。

$ rake spec files=spec/controllers/entries_controller_spec.rb
# (中略)
EntriesController
  - 取得したデータが表示されること

1 specifications (3 requirements), 0 failures, 0 errors

通りましたね。これで EntriesController は生成されたあとに Qiita API から投稿を取得し、取得した投稿の個数分の行が tableView に描画され、さらに title1title2 という文字列のラベルを持つことが確認できました。

EntryController のテスト

次は EntryController のテストです。

EntryController の役割は Qiita::Item クラスのオブジェクトを受け取って、UIWebView にその本文 (body プロパティ) を表示することですね。

Qiita::Item クラスの実装の詳細はここではあまり意識したくないので、モックにしましょう。

motion-stump という gem を使います。

Gemfile に以下の 1 行を追加し、ターミナルから bundle コマンドを実行してインストールしてください。

Gemfile

gem 'motion-stump'

ターミナルから以下を実行。

$ bundle

インストールできたらテストを書いてみます。

spec/controllers/entrycontrollerspec.rb

describe "EntryController" do
  before do
    # motion-stump でモック化する
    # スタブではなくてモックなので、body メソッドが一度も呼ばれなかったらエラーになる
    @entry = mock(:body, return: '<p>sample body</p>')
  end

  # tests メソッドはブロックを取れる
  tests EntryController do |controller|
    controller.entry = @entry
  end

  it 'WebView がひとつあること' do
    views(UIWebView).count.should.equal 1
  end

  it '本文が表示されること' do
    webview = views(UIWebView)[0]
    # UIWebView には表示している HTML を取得する方法が無いのでトリッキーだがこの方法を使う
    webview.stringByEvaluatingJavaScriptFromString('document.body.innerHTML').should.equal '<p>sample body</p>'
  end
end

コメントのように tests メソッドはブロックを取ることが出来ますので、ここでテスト前にコントローラに対して必要な初期化処理を行うことが出来ます。

実行してみましょう。

$ rake spec files=spec/controllers/entry_controller_spec.rb
# (中略)
EntryController
  - WebView がひとつあること
  - 本文が表示されること

2 specifications (2 requirements), 0 failures, 0 errors

無事、通りましたでしょうか?

イベントを発生させる

例えばボタンをタップしたときの動作をテストするにはどうすれば良いでしょう?

RubyMotion では tapflick などのメソッドを使ってそれらのイベントを発生させることが出来ます。

メソッド 説明
tap タップ
flick フリック
pinch_open ピンチアウト
pinch_close ピンチクローズ
drag ドラッグ
rotate 回転操作

では早速 EntriesController でセルをタップしたときの動作をテストしてみましょう。

entriescontrollerspec.rb に以下を追加してみます。

describe "EntriesController" do
  extend WebStub::SpecHelpers
  # (中略)

  it 'タップしたら...' do
    tap views(EntryCell)[0]
  end

end

これでテストを実行すると以下のようなエラーになると思います。

$ rake spec files=spec/controllers/entries_controller_spec.rb
# (中略)
EntriesController
  - 取得したデータが表示されること
  - タップしたら... [ERROR: NoMethodError - undefined method `pushViewController' for nil:NilClass]

NoMethodError: undefined method `pushViewController' for nil:NilClass
  entries_controller.rb:43:in `tableView:didSelectRowAtIndexPath:': EntriesController - タップしたら...
  ui.rb:101:in `proper_wait:'
  ui.rb:339:in `tap:'
  spec.rb:314:in `block in run_spec_block'
  spec.rb:438:in `execute_block'
  spec.rb:314:in `run_spec_block'
  spec.rb:329:in `run'

2 specifications (3 requirements), 0 failures, 1 errors

EntriesController 単体でテストしているため、navigationController が生成されていないことが原因でエラーになっています。

navigationController をモック化して、pushViewController:animated: メソッドが呼ばれることを検証してみましょう。

以下のようなコードになります。

describe "EntriesController" do
  extend WebStub::SpecHelpers
  # (中略)

  it 'タップしたら #pushViewController:animated: が呼ばれること' do
    controller.navigationController.mock!(:'pushViewController:animated:')
    tap views(EntryCell)[0]
    true.should.be.true # 何か specification を書かないとならないので
  end

end

少しいびつな感じですが、EntriesController のテストとしてできるのはここまでかと思います。

コントローラ・ビューのテストについては一つのコントローラ単体で検証できる動作に留め、複数のコントローラをまたぐような振る舞いをテストするのには次回説明する方法を使うのが良いでしょう。

まとめ

今回は RubyMotion に備わっている機能でどのようにコントローラ、ビューのテストができるのかについて説明しました。

さらに深く知りたい方は公式の Writing Tests for RubyMotion Apps (和訳) をご覧ください。

RubyMotion におけるビュー・コントローラのテストの特徴は、本来なら JavaScript で書かなければならない UIAutomation を Ruby で書くことが出来ることです。

今回はあまり触れることが出来ませんでしたが、デバイスを回転させたり、振ったりしたときの動作もテストできるようになっていますのでぜひ活用してみてください。

ただし繰り返しになりますが、ここで説明した機能はアプリケーションの完全なエンドツーエンドテストを目指して作られたものではありません。あくまでビューやコントローラの単体テスト、結合テストに用いるようにしてください。

次回はエンドツーエンドのテストを RubyMotion ではどのように書けばいいのかについてご説明したいと思います。

今回のサンプルコードは 201310-2.zip です。

Rubymotion
タグ:

記事をリクエストする

関連記事

コメント