Railsでテストを書く勘所
昨日はOSCに行ってきました。セミナーやブースはほとんど行かず、例によってRubyの会のあたりでだらだらしてたわけですが。
思いがけず師匠の師匠、id:t-wadaさんにもお会いできてびっくり。
で、そこでRailsとTDD(BDD)の話なんかしたので、一週間で思ったことをつらつらと。たぶん不正確というか、理解の足りないところもいろいろあるので、そのへんのツッコミをいただけると感謝です。
書いてたら長くなったのでagenda
- モデルのテストでは、とにかくロジックを書いたらテストを書く*1。def..endブロック(wを書いたら必ずテストもあるはず。
- コントローラのテストでは、基本的にリクエストを受けてから表示対象のオブジェクトを導出するまでをテストしたい。
- ビューのテストでは、assert_tag()とかresponse.should_have_tag()でごりごりやりたい誘惑に駆られるが、そこに突っ込むのはかなり修羅の道。
- ビューをテストするのはtest/functionalなのかtest/integrationなのか、どっちがふさわしいのかはよくわからない。
モデルのテスト aka test/unit or spec/models
- モデルのテストでは、とにかくロジックを書いたらテストを書く。def..endブロック(wを書いたら必ずテストもあるはず。
- validationやassociationのテストは入らないようにも思うけど、そのへんが不安だったらテストを書いた方がいい。
- [受け売り]不安なところはテストする、というかテストは不安に立ち向かうためにある。
- 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
残課題というかこれから考えること
- 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