Railsでテストを書く勘所

昨日はOSCに行ってきました。セミナーやブースはほとんど行かず、例によってRubyの会のあたりでだらだらしてたわけですが。
思いがけず師匠の師匠、id:t-wadaさんにもお会いできてびっくり。

で、そこでRailsとTDD(BDD)の話なんかしたので、一週間で思ったことをつらつらと。たぶん不正確というか、理解の足りないところもいろいろあるので、そのへんのツッコミをいただけると感謝です。

書いてたら長くなったのでagenda

  • モデルのテストでは、とにかくロジックを書いたらテストを書く*1。def..endブロック(wを書いたら必ずテストもあるはず。
    • RailsMVCコンポーネントの中では一番テストし易いので、そういう意味でもモデルを厚くすると幸せになりやすい。
  • コントローラのテストでは、基本的にリクエストを受けてから表示対象のオブジェクトを導出するまでをテストしたい。
  • ビューのテストでは、assert_tag()とかresponse.should_have_tag()でごりごりやりたい誘惑に駆られるが、そこに突っ込むのはかなり修羅の道。
    • ビューをテストするのはtest/functionalなのかtest/integrationなのか、どっちがふさわしいのかはよくわからない。

モデルのテスト aka test/unit or spec/models

  • モデルのテストでは、とにかくロジックを書いたらテストを書く。def..endブロック(wを書いたら必ずテストもあるはず。
    • validationやassociationのテストは入らないようにも思うけど、そのへんが不安だったらテストを書いた方がいい。
      • [受け売り]不安なところはテストする、というかテストは不安に立ち向かうためにある。
  • ビジネスロジックをモデルに集約する、というのは常道で、そうすべき理由もいろいろなものがありますが、特にRailsではコントローラやビューのテストと混ざるといろいろ複雑になるので、テストをやりやすくするためにもモデルを厚くするよう心がけた方がいいとおもう。
  • 開発の流れは、↓みたいにするのが良いのかな。

1. あるべきインターフェースで、普通の引数を渡してアサーションする。

<address_spec.rb>
 address.contacts(:nickname => true).should_equal ["moro", "moronatural_at_gmail.com"]

2. クラスを作ったりメソッドを定義したりしてFakeする

<address.rb>
 def contacts(opts={})
   ["moro", "moronatural_at_gmail.com"]
 end

3. コミットする
4. 別パターンのテストを書いて赤くなる。

<address_spec.rb>
 address.contacts(:nickname => false).should_equal ["MOROHASHI Kyosuke", "moronatural_at_gmail.com"]

5. テストが通るようにする。

<address.rb>
 def contacts(opts={})
   displayed_name = opts[:nickname] ? "moro" : "MOROHASHI Kyosuke"
   [displayed_name, "moronatural_at_gmail.com"]
 end

6. グリーンバーをみたらコミットする。
7. リファクタリングをして重複を取り除く

<address.rb>
 def contacts(opts={})
   displayed_name = opts[:nickname] ? self.nickname : self.realname
   [displayed_name, self.mail_address]
 end

8. グリーンバーをみたらコミットする。

コントローラのテスト aka test/functional or spec/controllers

  • コントローラのテストでは、基本的にリクエストを受けてから表示対象のオブジェクトを導出するまでをテストしたい。
    • 基本は post / get にHashを渡してリクエストを送り、結果をassigns()でとってassertする感じかと。
    • レスポンスが普通のHTMLなのか、リダイレクトなのか位は確認しても良いかも。
<address_controller_test.rb (test/unitスタイル)>
 get :list
 assert_response :success
 address = assigns(:address)
 assert_not_nil address
 assert_equal @moro_address.nickname, address.nickname
 ...
  • 関連の導出のテストも必要ならばやってよい。
    • モデルのテストは「関連が付けられているかどうか」、コントローラのテストでは、「目的のモデルが導出されているかどうか」をテストする。意味合いが微妙に異なりますね。
<addressbook_controller_spec.rb (rspecスタイル)>
 get :list, :nickname => "moro"
 assert_response :success

 user = assigns(:user)
 user.nickname.should_be_equal "moro"
 ...
 addressbook = user.addressbook
 addressbook.should_not_be_nil
 addressbook.should_have(5).contacts
 ...
      • eager loadingしてるかどうかをテストするかどうかはどうすれば善いんでしょうか?
  • このへんのテストを簡単にFakeできるのがRubyのよさ。グリーンバーにすぐ会えて嬉しい。
<addressbook_controller.rb>
 # [Fake]
 def list
   @user = User.find_by_nickname("moro")
   @user.addressbook.instance_eval do 
     def contacts ; (1..5).to_a ; end
   end
 end

ビューのテスト aka test/functional or spec/view

  • ビューのテストはtest/unit派の人はfunctional testに書くことになるのかな。rspecのビューのテストって何書くんだよ、って人(私含む)もこのへんをテストすれば良いんじゃないかなぁ、と思ってます。
  • 入力系では、ユーザが入力するためのフォーム部品があるかどうかを確認する。
<address_controller_test.rb (test/unitスタイル)>

 assert_tag :tag => "input", :attributes => {:type => "hidden", :name => "user_id"}
 assert_tag :tag => "input", :attributes => {:type => "hidden", :name => "_session_id"}
 assert_tag :tag => "input", :attributes => {:type => "text",   :name => "contacts_search_query"}
  • 参照系ではかならず存在するはずの情報とあと次画面へのリンクですかね。
<addressbook_list_spec.rb (rspecスタイル)>

 # <a href="/addressbook/new">新規アドレス帳</a>みたいなのがあること
 response.should_have_tag :tag => "a", :attributes => {:href => %r!/addressbook/new!}
  • assert_tag()とかresponse.should_have_tag()でごりごりやりたい誘惑に駆られるが、そこに突っ込むのはかなり修羅の道。
    • やりはじめると簡単にこんなのになります。
 assert_tag :tag => "ul",
            :attributes => {:class => "address_list", :id => "address_list"},
            :children => {:count => 1..5,
                          :only => { :tag => "li", :attributes => {:class => "address_odd"}},
            :children => {:count => 1..5,
                          :only => { :tag => "li", :attributes => {:class => "address_even"}}
    • これは読んでも直感的じゃないし、そもそも役に立つのか、と。
      • あと、ちょっと画面を変えただけでのきなみ赤くなって泣きそうになります。
      • スーツを着た人が完全に定義された画面設計書とHTMLを持ってきてくれるような感じだと、assert_tagでパースするのもいいのかも。ほんとか?
  • 要は、「開発/設計としてのテスト」ではユーザがシステムを使うために必ず必要となるビューの部品に絞ってテストをしよう、と言う事です。
    • この手のはあること自体はずっと変わらない、というか変わるときは大きめ仕様変更なのでそれなりのコストを支払うのはしょうがない。
    • QAテストで画面表示が崩れていないこと、なんかを確認するのは別のやりかたにしてしまった方がいいと思います。

結合テスト aka integration test

  • 結合テストの観点は、「ユーザがブラウザからたどる道筋をトレースするようなテスト」です。
  • get/postでは、パス文字列を直接渡すようにするのがよいと思ってます。
    • ビューのテストと被っちゃうんですが、次画面に進むためのリンク/ボタンがあるかどうかも見ておくとよいかも。
      • でも重複を無くしたいよなぁ。どっちに含めるのがいいんだろ? > リンク
 # get(:controller => "contacts", :action => "list", :id => 12)ではなく。
 get "/contacts/show/12"
 assert_response :success
 assert_template "list"

 assert_link_to_next_screen "/contacts/edit/12"
 assert_link_to_next_screen "/contacts/destroy/12"

..
def assert_link_to_next_screen(link)
  ...
  assert_tag :tag => "a", :attributs =>{:href => link}
end
  • cookieやセッションに予期したものが入ってるかどうか、とかもここで。
    • assigns()での値の取りだしよりも、ユーザに取って見える情報*2をassertするの優先で。
  • integration testはまだこれというポイントがみえてません。あとで書ければいいなぁ。

残課題というかこれから考えること

  • Seleniumを使ったQAテストをもう少しちゃんとおぼえる。
  • 生成されたHTMLをどこでassertするべきか。いまのところビューとコントローラとITとでほんとに自信を持った切り分けができていない。
  • rspecのよみかたは「りすぺっく」と「あーるすぺっく」のどっちが正しいの?
  • assert_selectとassert_domを試してみて喜んで、rspecにもresponse.should_have_domとかが追加されるのを心待ちにする。
  • コレクションへのテストをスマートに書く方法を考える。
    • いまはこう書くけど
 specify "導出されたaddressはすべていまも使える(address.active == true)ものであること do
   addresses.each do |address|
     address.should_active
   end
 end
    • こう書けたらカッコいい。
 specify "導出されたaddressはすべていまも使える(address.active == true)ものであること do
   all_of(addresses).should_active
 end

その他ポイント

  • テストの件数が増えてくるとテストDBの速度がテスト実行時間にかなり影響してくる。
    • テスト100件、fixture 150レコードくらいでSQLite3の物理DB vs. in memory DBでは2倍弱のパフォーマンス差がある感じ。
    • RAMディスクにSQLite3の物理DBを置くというのも有り。当然といえば当然だけど、in memoryといい勝負する位のパフォーマンスは出る。
      • MacOS XでRAMディスクを作ってマウントするためのRakeタスクをあとで書く。
  • rspecいいよrspec

*1:正確にはロジックを書きたくなったら、でしょうね

*2:セッションの中身は見えないけどまぁそれはそれで