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。
- http://tutorial.rubymotion.jp/8-testing/
- http://rubymotion.jp/RubyMotionDocumentation/articles/testing/
- https://speakerdeck.com/pchw/rubymotion-1-dot-15dezhui-jia-saretatestzhou-rifalsehua
- http://rubymotion-wrappers.com/
公式のドキュメントにはテストの動かし方は書いてあっても、どのようにテストを書けば良いかは書いてない。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。
Ruby に Awesome Print という、print デバッグの出力をすごい良い感じにするライブラリがありますが awesome_print_motion はそれの RubyMotion 版。
なんだかんだで RubyMotion でも print デバッグは多用するので、入れておくと捗るでしょう。