Hatena::ブログ(Diary)

moroの日記 このページをアンテナに追加 RSSフィード

2006-10-29

Railsでテストを書く勘所

昨日はOSCに行ってきました。セミナーやブースはほとんど行かず、例によってRubyの会のあたりでだらだらしてたわけですが。

思いがけず師匠師匠id:t-wadaさんにもお会いできてびっくり。

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

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

モデルテスト 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

<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

その他ポイント

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

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

yuguiyugui 2006/10/29 21:54

def all_of(*parts)
proxy = Object.new
(class << proxy; self end).class_eval do
define_method(:method_missing) do |msg, *args|
parts.each do |part|
part.send(msg, *args)
end
nil
end
end
proxy
end

moromoro 2006/10/30 11:49 ktkr
本家へのパッチよろ ><

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証