RubyMotion のテスト、継続的インテグレーション

昨日は RubyMotion のもくもく会でした。

先日の RubyMotion Kaigi 2013 で 実践RubyMotion という題目で発表したのだけど、テストについてはprintデバッグ上等だ、このクソムシがとか言ってかなり適当に済ませてしまった。ので、もくもく会ではテスト周りに手をつけるぞと思い、そういえば Travis CI が RubyMotion に対応してたのも思い出し RubyMotion のテストを Travis CI で回すのを検証した。

が、手間取るかと思った Travis CI 周りはとっても簡単で、.travis.yml に

language: objective-c

と書くだけであっさり動いてしまった。

というわけで RubyMotion アプリの継続的インテグレーションは .travis.yml を一行書けば完了です。終わり・・・じゃあまったくブログ記事として成立しないので、RubyMotion のテスト周りについてちょいちょい書いておこう。ちなみに検証に使ったレポジトリは以下にあります。

ドキュメント

この辺読めば基本的なところは OK よ、というドキュメントが以下の3つ + 1。

公式のドキュメントにはテストの動かし方は書いてあっても、どのようにテストを書けば良いかは書いてない。1本目の URL は RubyMotion といえばこの人 Clay Allsopp さんのチュートリアル文書を、watson ほかが翻訳したもの。2本目のは、その公式文書の日本語訳。これも watson さん。

3本目のはちょうど一年前の第一回RubyMotion 勉強会での pchw さんの発表スライド。view や controller のテストの書き方について解説されている。もうあれから一年経ってしまいました。

4本目は、有名な RubyMotion のラッパライブラリのリンク集。以下で紹介するテスト系のライブラリはだいたいこのリンク集経由で集めてきました。

テストの始め方

RubyMotion のテストは

% rake spec

これで動きます。テストは

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

   it "has no window under testing" do
     @app.windows.size.should == 1
   end
 end


こんな感じで例によって RSpec 風である。風、というのはこれは実際にはピュア RSpec ではなく Bacon という RSpec のクローンだそうです。

テストを行う場合 app_delegate.rb に

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    return true if RUBYMOTION_ENV == 'test' # これ


環境変数が test だったらすぐに return するようにしておくと良い。テストはアプリを完全にビルドして行うものではないので、テストのときはそれをしない、ということですね。これでビルドの時間が短縮されます。テストには影響はありません。

テストに色を付ける

RSpec だと .rspec に --color とかで色が付きますが、Bacon なのでそのオプションは効かない。rm-redgreen を使う。

  • rm-redgreen を git clone する
  • spec/00-redgreen.rb を spec にコピー
  • app/*.rb を app にコピー

後、rake spec すると色がつく。ファイルを色々コピーして使うというのがちょっとアレですが。

ついでに 00-redgreen.rb の style オプションを

style = :full

とすると、出力が良い感じになります。


motion-guard

RSpec のテストなら guard を使いたい。ファイルを更新したら自動で spec 流したりできる、アレです。例によって通常の guard-rspec ではダメなので、guard-motion を使います。

% guard init motion
% guard start

これで RubyMotion 関連のファイルを guard が監視して更新があると関連するテストが走るようになります。素の guard 同様、なるべく更新した箇所に関するテストだけを実行するよう良きに計らってくれます。

RubyMotion のテストはテスト毎にアプリのビルドが走って少し時間がかかるので、手動でやらずに guard で勝手に流れるようにしておくとだいぶストレスが軽減されて良い感じです。

Travis CI

Travis CI でのテストの仕方は冒頭で書いた通り。.travis.yml を用意して Github に push するだけ。

このとき BubbleWrap や sugarcube などのサードパーティライブラリはどうなるか。Travis CI はテスト前のビルドに際して毎回 bundle を実行するので、Bundler 経由で入れておけばちゃんとインストールやビルド含め、テストされます。検証済み。

Cocoapods に関しては検証してないですが、そもそも rake 時に pod ファイルを取ってくる作りなのでこれも問題なさそう。もしかすれば .travis.yml をいじって事前に pod setup する必要はあるかも?

コントローラのテスト

テストの実行の仕方は分かった、じゃあ実際にテストはどう書けばいいの? というところを以下簡単にご紹介。

まずはコントローラやビューのテストですが

class MainViewController < UIViewController
  def viewDidLoad
    super
    margin = 20

    self.view.backgroundColor = UIColor.whiteColor

    @button = UIButton.rounded_rect.tap do |b|
      b.setTitle('Hello, RubyMotion!', forState:UIControlStateNormal)
      b.accessibilityLabel = "Hello Button"
      b.frame = [[margin, 100], [view.frame.size.width - margin * 2, 42]]
      b.on(:touch) do |event|
        @was_tapped = true
      end
    end

    view << @button
  end
end

こんなコントローラに対するテストを

describe MainViewController do
  # MainViewController をテスト対象に ⇒ インスタンス化される
  tests MainViewController

  it "should not be nil" do
    # viewメソッドを使って accessibilityLabel = "Hello Button" の view を見つける
    view("Hello Button").should.not.be.nil
  end

  it "has right title" do
    view("Hello Button").currentTitle.should.equal "Hello, RubyMotion!"
  end

  it "changes instance variable when button is tapped" do
    # ボタンを実際にタップ
    tap "Hello Button"
    controller.instance_variable_get("@was_tapped").should == true
  end
end

こんな風に書きます。

ポイントになるところはコメントにある通りですが、おもしろいのは UIView の accessibilityLabel の値で、実際にインスタンス化されている任意のサブビューを検索できるところですね。accessibilityLabel はその名の通りアクセシビリティのためのラベルで、音声読み上げなどに使われるテキストですが、RubyMotion ではこれをテストに使ってます。とはいえ、ドキュメントにもある通り、本来の用途はテキスト読み上げなのでここに余計なものは書かない方が良いでしょう。

それから、tap。RubyMotion のテストフレームワークには tap、flick、pinch_open、drag … など実際のアプリ上の操作をエミュレートするメソッドが用意されています。これを呼ぶと、アプリ上で実際にボタンが押される。この辺りは iOS SDK の UIAutomation の機能を RubyMotion で使えるようにした、というものだそうです。

あまり自分は詳しくないですが iOS の場合は UIAutomation を使ったテストは JavaScript で書くようですが、RubyMotion なら 普段通り ruby で書けて嬉しい、とのこと。

とまあ、こんな具合で UI のテストが書けますが、UI のテストをどこまで書くべきかというのは例によって正解のない議論ですね。その辺はここで論じるのはやめておきましょう。いずれにせよ RubyMotion のテストフレームワークなら、UI から任意の処理キックしてそれをテストするなんてことが簡単ですよ、ということぐらいは覚えておくといいと思います。

モデルのテスト

モデルのテストも書いてみましょう。

class User
  attr_accessor :name
  attr_accessor :email
end

という Plain Old なモデルクラスに対するテストを

describe User do
  before do
    @user = User.new
  end

  it "has appropriate attributes" do
    @user.should.respond_to :name
    @user.should.respond_to :email
  end
end

こう書きました。何の変哲もないですね。コントローラのテスト時に使っていた、コントローラや UI 部品に関するものを使わないで素でテストを書くだけ。

ここでのコードはクラスもテストも全く RubyMotion に依存していないので、RubyMotion をビルドせずにテストしても良いようにも見えますが、それは NG です。このテストをきちんと RubyMotion のテストプロセス上でテストする。なぜなら RubyMotion のクラスは NSObject ほか、Objective-C ランタイムのオブジェクトを継承していて、ピュア Ruby のそれと違うから。RubyMotion 実行環境の上でテストしておくのは必須でしょう。

モデルのテストをするのにアプリのビルドが必要だなんて、という辺りは先の app_delegate での工夫と guard-motion を使えばあまり気にならない程度の時間で終わるようにはなります。

スタブ/モック

スタブやモックは motion-stump あたりが良さそうです。

@user.stub!(:hello, :return => "Hello, naoya")
@user.hello.should.equal "Hello, naoya"

こんな感じでスタブできました。

HTTPリクエストのスタブ

HTTPリクエスト専用のスタブの webstub も良い感じです。

class HttpClientViewController < UIViewController
  def viewDidLoad
    super
    view.backgroundColor = UIColor.whiteColor

    @button = UIButton.rounded_rect.tap do |b|
      b.accessibilityLabel = "Start HTTP"
      b.frame = [[20, 100], [view.frame.size.width - 20 * 2, 42]]
      b.on(:touch) do |event|
        BW::HTTP.get('http://www.example.com/') do |response|
          p response
        end
      end
    end
    view << @button
  end
end

こういう、BubbleWrap で HTTP リクエストを行ってるコントローラなんかをどうテストするか、というときに

describe HttpClientViewController do
  extend WebStub::SpecHelpers
  tests HttpClientViewController

  it "has a button" do
    view("Start HTTP").should.not.be.nil
  end

  it "sends a http request when button is tapped" do
    # webstub で HTTP をスタブ
    stub = stub_request(:get, 'http://www.example.com/')
    tap "Start HTTP"
    stub.should.be.requested
  end
end

と WebStub を使うとあっさりスタブできてしまう。WebStub でのレスポンスは色々カスタマイズできるので、非同期テストなんかもそれなりにうまく回せそうです。

話は逸れますが、RubyMotion 専用のライブラリなのに "webstub" なんて名前空間使っちゃっていいんでしょうか・・・。まあそれを言ったら sugarcube や bubble-wrap もなんですけど。ruby はこの辺カオ・・・寛大ですね。

awesome_print_motion

最後に tips。

RubyAwesome Print という、print デバッグの出力をすごい良い感じにするライブラリがありますが awesome_print_motion はそれの RubyMotion 版。

なんだかんだで RubyMotion でも print デバッグは多用するので、入れておくと捗るでしょう。

まとめ

というわけで RubyMotion のテスト周りでした。

調べてみると guard もあるし CI も回せるし、スタブもモックもあるし全部 ruby で書けるしテストを書くことそのことについての痛みはほとんどないんだなと思いました。アプリのテストは面倒だという先入観がありましたが、UIAutomation + RSpec でのテストとか書いてみるとラピッドに書けて、実に楽勝ですね。こりゃあ、汎用ライブラリやモデル周りのテストはしっかり書いて行かなきゃな、と思った次第です。