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

実践!RubyMotion

第五回 RubyMotionでHTTPや非同期処理を含むユニットテストを行う

Satococoa

第五回 RubyMotionでHTTPや非同期処理を含むユニットテストを行う

前回はこれまで作ったアプリケーションのリファクタリングを行い、テストが書けるところまで進めてきました。いよいよ今回からテストを書いていきたいと思います。

RubyMotion のテストについて

RubyMotion 付属のテストフレームワークは RSpec のクローンである Bacon を Mac 向けに拡張した MacBacon です。

この MacBacon を使うことでモデルのユニットテストはもちろん、View や Controller に対する UIAutomation 相当のテストを行うことも出来ます。

最近 @ryo_katsuma さんの Pull Request によって context使えるようになりましたが、細かいところでは RSpec とは異なっていますので RSpec に慣れている方は注意してください。

初めてのテストの実行

実は motion create したときにテストケースがひとつ自動的に作られています。spec/main_spec.rb を開いてみてください。

describe "Application 'RMQiita'" do
  before do
    @app = UIApplication.sharedApplication
  end

  it "has one window" do
    @app.windows.size.should == 1
  end
end

このテストは現時点で既にパスするようになっています。実行してみましょう。

$ rake spec
# (ビルドのログは省略)
Application 'RMQiita'
  - has one window

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

ご覧のようにテストをパスしているようです。テスト結果の出力に色をつけることもできます。以下のように output オプションを渡して実行してみましょう。

$ rake spec output=colorized
.

Finished in 0.00 seconds.
1 tests, 1 assertions, 0 failures, 0 errors

色付きの出力結果
色付きの出力結果

失敗したときの出力も見ておきたいので、spec/main_spec.rb を少し変更してわざと失敗させてみます。

it "has one window" do
  @app.windows.size.should == 2 # 1 => 2 に変更
end

変更したら再度テストを実行してみます。

$ rake spec output=colorized
F

Bacon::Error: 1.==(2) failed
  spec.rb:694:in `satisfy:': Application 'RMQiita' - has one window
  spec.rb:708:in `method_missing:'
  spec.rb:315:in `block in run_spec_block'
  spec.rb:439:in `execute_block'
  spec.rb:315:in `run_spec_block'
  spec.rb:330:in `run'


Finished in 0.35 seconds.
1 tests, 1 assertions, 1 failures, 0 errors

色付きの出力結果(失敗例)
色付きの出力結果(失敗例)

正しく失敗してくれましたね。

変更した部分を元に戻し、テストをパスする状態にしたら次に進みましょう。

Qiita::Item のテストを書く

テストの動かし方がわかったところで、実際に Qiita::Item クラスのユニットテストを書いてみましょう。

spec/models/qiita/ ディレクトリを作り、その中に item_spec.rb を以下の内容で作成してください。

spec/models/qiita/item_spec.rb

describe "Qiita::Item" do
  before do
    @data = {
      'title' => 'title',
      'user' => {'url_name' => 'name'},
      'updated_at_in_words' => '2 days ago',
      'body' => '<p>body</p>'
    }
    @item = Qiita::Item.new(@data)
  end

  it 'should be created' do
    @item.title.should.equal      @data['title']
    @item.username.should.equal   @data['user']['url_name']
    @item.updated_at.should.equal @data['updated_at_in_words']
    @item.body.should.equal       @data['body']
  end
end

与えられた引数で初期化された @item オブジェクトの各属性が意図通りになっていることを検証しています。

@dataQiita API から取得した個別の投稿のデータ (json) を Hash にしたものを想定しています。

実行すると、無事テストをパスするはずです。なお下記のようにfiles オプションを使うと特定の spec のみ実行することが出来ます。"," カンマで区切ることで複数ファイル指定可能です。

$ rake spec files=spec/models/qiita/item_spec.rb
Qiita::Item
  - 属性が正しいこと

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

繰り返しになりますが、実際はテストを先に書く、もしくは書きながら実装を進める方が好ましいのですが今回は後付けでテストを用意しました。

MacBacon で使用できる matcher は MacBacon の README.md を参照するといいでしょう。

Qiita::Client のテストを書く

次は Qiita::Client.fetch_tagged_items メソッドのテストを書いてみましょう。

このメソッドを書くにあたって、以下の 2 点が少し難しいところです。

  • 外部の API からデータを取得する
  • データ取得後 (非同期で) メソッドに与えられたブロックを実行する

ひとつずつ解決していきましょう。

WebStub を使う

外部に依存するメソッドのテストを書くときはモックやスタブを用いると良いでしょう。

RubyMotion で Web の API をスタブ化するには WebStub という gem が使えます。

早速 WebStub をインストールします。Gemfile に以下を書き加え、bundle コマンドを実行してください。

Gemfile

gem 'webstub'
$ bundle
Resolving dependencies...
Using rake (10.1.0)
Using bubble-wrap (1.3.0)
Using webstub (0.6.1)
Using bundler (1.3.5)
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.

無事インストール完了しました。

WebStub を使うと以下のように任意の HTTP アクセスをスタブ化することができます。

stub_request(:get, "http://example.com/foo").
  to_return(body: "Hello!", content_type: "text/plain")

また、インターネットに接続できない状況も作り出すことが出来ます。

stub_request(:get, "http://example.com/foo").
  to_fail(code: NSURLErrorNotConnectedToInternet)

この状態で http://example.com/foo に GET リクエストを送ると NSURLErrorNetConnectedToInternet エラーを出します。

wait_max, resume を使う

次は非同期処理のテストですが MacBacon に備わっている wait_max, resume というメソッドを使うと実現できます。

簡単な使い方は以下のとおりです。

some_method_with_block {|some_data|
  @data = some_data
  # 非同期で実行されるブロックが終わったら resume する
  resume
}

wait_max 1.0 do
  # resume されるか、1.0 秒経過したら実行される
  @data.should.equal foo # ここで data を検証
end

これで非同期処理のテストもできます。

Qiita::Client のテスト

WebStub, wait_max, resume を使って Qiita::Client のテストを書くと以下のようになります。

spec/models/qiita/client_spec.rb

describe "Qiita::Client" do
  extend WebStub::SpecHelpers

  describe '.fetch_tagged_items' do
    before do
      # こんなデータが API から返される
      @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>'
        },
      ]
      @tag = 'RubyMotion'
    end

    context 'API へのアクセスに成功するとき' do
      before do
        # ここで API をスタブ化
        stub_request(:get, "https://qiita.com/api/v1/tags/#{@tag}/items").
          to_return(json: @data)
      end

      it '取得したJSONをもとにオブジェクトが作られる' do
        Qiita::Client.fetch_tagged_items('RubyMotion') {|items, message|
          # ブロックには API から取得したデータを元に作られた
          # Qiita::Item の配列と、エラーメッセージが渡される。
          # あとで検証するためにインスタンス変数に入れる。
          @items   = items
          @message = message
          resume
        }

        wait_max 1.0 do
          # ここで @items, @message の検証を行う。
          @items.count.should.equal @data.count

          @items.each_with_index {|item, index|
            item.title.should.equal      @data[index]['title']
            item.username.should.equal   @data[index]['user']['url_name']
            item.updated_at.should.equal @data[index]['updated_at_in_words']
            item.body.should.equal       @data[index]['body']
          }

          @message.should.be.nil
        end
      end
    end
  end
end

上記は API が正常にレスポンスを返してきた場合のテストのみです。例えば 400 系のエラーをエラーメッセージとともに返してきたらどうなるでしょう。また、インターネットに接続できない場合のテストはどうすればよいでしょう。

WebStub を適切に使用することでこのような状況のテストも書くことが出来るはずです。ぜひ挑戦してみてください。

参考資料

RubyMotion のテストについてもっと知りたい方は、まずは以下の公式ドキュメントを参考にすると良いでしょう。

Guard を使って自動的にテストを実行させる方法もあります。Qiita のRubyMotion で Guard を使って自動的にテストを実行する方法を参考にしてください。

まとめ

今回は RubyMotion でのユニットテストの書き方・実行の仕方について書きました。

次回は UI のテストに踏み込んでみたいと思います。

今回のサンプルコードは こちらからダウンロード できます。

Rubymotion

記事をリクエストする

関連記事

コメント