読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

Ruby on Railsのテストの書き方 (モデルの単体テストと,コントローラの機能テスト)

Ruby on Rails ruby テスト 設計


Ruby on Rails のテストの書き方のまとめ。


RSpecを使わない,素の

  • unit test (モデルのテスト)
  • functional test(コントローラ+ビューのテスト)

について,どう書いたらいいのか,どこの情報を参照したらよいのか,などを列挙。

Rails入門者が初めてテストを書く際に迷わせないようにするためのガイドライン形式。




Railsのバージョンは1.2系を想定。

総説

一度は目を通すべき,入門用のリンク集:

RubyOnRails を使ってみる 【第 6 回】 テストの書き方
http://jp.rubyist.net/magazine/?0013-RubyOnRails

Railsでテストを書く勘所
http://d.hatena.ne.jp/moro/20061029/1162116778

Railsでのテスト
http://d.hatena.ne.jp/akm/20061126/1164552662


平易なサンプルコード:

Ruby on RailsでTestをしよう ⇒ Ruby on Rails で units test の仕方
http://maskana.homedns.org/rails/pro/detail/test/48

Ruby on RailsでTestをしよう ⇒ Ruby on Rails で functionals test の仕方
http://maskana.homedns.org/rails/pro/detail/test/49

(1)モデルの単体テスト

決まり(ファイルの場所など):

テストコード

  • /test/unit/モデル名(単数形)_test.rb
  • テストメソッドの名称は test_ で始める

テストデータフィクスチャ):

  • /test/fixtures/テーブル名(複数形)_test.rb
  • 個々のテストメソッドの実行直前に,毎回このデータだけが投入される
  • フィクスチャ内の個々の投入レコードには,名前(フィクスチャ名)をつけられる

テスト実行方法

  • rake test:units で全実行
  • ruby test/unit/ファイル名 で個別に実行


テストメソッドの作成方針:

  • DBに対して,期待通りの操作が行なえているかどうかを検証する。
  • モデルの全メソッドを網羅する。モデルの1メソッドに対して,最低1つ以上のテストメソッドを書く。
    • Railsではモデルにビジネスロジックが詰め込まれ,要となるので。
  • モデルの1メソッドが,大きく異なった複数の振る舞いをする場合,それぞれの振る舞いに対して個別にテストメソッドを書く。
    • ※しかし,それだったらモデルの1メソッドをリファクタリングして複数メソッドに分割したほうがよいだろう。どういう基準でテストメソッドを分割しているのか,余計な説明が必要になってしまうから。
  • モデルメソッド以外に,モデルのバリデーションルールなどの個々の性質・挙動もテストコード作成対象にしてよい。


具体的なテスト内容:


Test::Unitを使う。(Ruby on Railsに特化したテスト方法ではない)

  • assert系のメソッド一覧:
  • assert( hoge ) : hogeがtrueであることを要求
  • assert_equal( expected, actual ) : expected == actualであることを要求
  • assert_not_equal( expected, actual ) : expected != actualであることを要求
  • flunk( メッセージ ) : 常に失敗
  • ほかのテストメソッドは上記の応用で済ませる事が可能


もしテスト対象が参照系のメソッドであれば,テストメソッド中では:

  1. そのメソッドを使って,現在のDB内容を読み込む。
  2. (1)の返り値が,期待通りの構造+内容かどうかをassertで検証。
    • Fixtureに書いてある通りの内容か。


もしテスト対象が更新系のメソッドであれば,テストメソッド中では:

  1. そのメソッドを使って,DBの状態を変更する。
  2. 参照系のメソッド(※find(:all)のような基本的なメソッド)を使って,現在のDB内容を読み込む。
  3. (2)の返り値が,期待通りの構造+内容かどうかをassertで検証。
    • Fixtureに対して,(1)の操作が加えられたような内容か。


もしテスト対象がその他のモデルの性質であれば,テストメソッド中では:

  1. newでモデルのインスタンスを作成する。
  2. (1)のモデルオブジェクトそのものを操作する。(アソシエーション設定など)
  3. (2)の後に,モデルオブジェクトが期待通りの性質を持っているかどうかをassertで検証。


特筆事項:

  • setup()内に共通の事前処理を書ける。
  • テーブル名( :フィクスチャ名 ) で,フィクスチャデータ内の特定のレコードをActiveRecordとして参照できる。

(2)コントローラの機能テスト

決まり(ファイルの場所など):

テストコード

  • /test/functional/コントローラ名_test.rb
  • テストメソッドの名称は test_ で始める(モデルと同じ)

テストデータ(fixture):

  • モデルと同じ

テスト実行方法

  • rake test:functionals で全実行
  • ruby test/functional/ファイル名 で個別に実行


テストメソッドの作成方針:

  • リクエストに対して,期待通りのレスポンスが返ってくるかを検証する。
  • publicな全アクションを網羅する。


テストのボリューム:

  • 増やすべき点:
    • クライアントコード(JavaScript)が絡むような性質のテストは,Ruby側ではなく,Seleniumに任せる必要がある。*1
    • とは言っても,Seleniumのテストは実行に時間がかかってしまう。
    • Seleniumのテストケースが膨れ上がらないように,JavaScriptが絡まない部分はRails側のコントローラテストを増やすことによって吸収しておく。
  • 増やすべきでない点:
    • 画面の詳細なレイアウトは,開発において最も変更されやすい点なので,テスト項目に含めてはならない。(「変更に強くする」ためのテストなのに,それでは逆に,テストのせいで変更に弱くなってしまう。)
    • DOM要素が存在するかどうかの判定ぐらいに留めておく。


具体的なテスト内容:


リクエスト生成(get/postなど)は,ActionController::TestProcessモジュール内のメソッドを利用する。


検証は,ActionController::Assertionsモジュール内のメソッドを利用する。


サンプルなど:


手順:

  • (1)リクエストを生成・実行する。
    • (1-1)まず,HTTPパラメータやセッションデータを準備。
    • (1-2)それらのパラメータを使って,アクションへのHTTPリクエストをシミュレートする。
      • get :アクション名, パラメータ, セッション
      • post :アクション名, パラメータ, セッション
      • xhr :リクエストメソッド, :アクション名, パラメータ, セッション
  • (2)(リクエストの結果としてRailsが返却してくる)レスポンスを検証する。
    • HTTP status… assert_response :success もしくは :redirect
    • redirect先… assert_redirected_to :controller => コントローラ名, :action => アクション名
    • DOM要素の存在判定… assert_tag :tag => "span", :attributes => { :id => "hoge" }
    • DOM要素の非存在判定… assert_no_tag :tag => "span", :attributes => { :id => "hoge" }
  • (3)コントローラの事後状態を検証する。
    • ビューテンプレートに渡されるインスタンス変数(@hoge)が期待通りの値かどうかをassertで検証。
    • インスタンス変数は assigns( 変数名 ) で取ってこれる。
  • (4)必要に応じて,DBの事後状態を検証する。
    • モデルの参照系メソッドを呼び出して,コントローラのアクションが期待通りのDB操作を行なったかどうかをassertで検証。http://www.thinkit.co.jp/cert/article/0605/2/4/4.htm
    • ただし,コントローラ層のテストに,モデル層のメソッド呼び出しを混在させてよいかどうかは十分考えること。

(3)その他,共通事項

  • 全テストの一斉実行
    • rake test
  • テストの必要性:
    • コンパイルが無いので,せめて全行を通して実行すべき。

Railsでのテスト
http://d.hatena.ne.jp/akm/20061126/1164552662

  • コントローラやビューではなく、モデルで実装できる部分はできるだけモデルに書いておくと、テストを書くのが楽になる


Ruby on Railsの「えせMVC」の弊害
http://satoshi.blogs.com/life/2009/10/rails_mvc.html

  • 単にDBを抽象化しただけ(ORM)だと,真の意味でのMVCではない。「ビジネスロジックのカプセル」という意味でモデル層を書くこと。
  • モデル層は,データの整合性(トランザクション)の責任を100%負うこと。複数モデルのメソッドをコントローラからバラバラに呼び出してしのいでいるような状況はNG。DAOの意味でのモデルクラス達の上に一皮かぶせた,親玉のモデルクラスが必要。(親モデル/子モデルに分ける)


えせMVCについてそろそろ一言言っておくか
http://d.hatena.ne.jp/higayasuo/20091013/1255408723

  • モデルは厚くすべきだが,厚すぎたら把握しづらい。特定のユースケースでしか使われないメソッドは,モデルではなく特定のコントローラに置いてあってもよい。その辺はバランスを取るべき。
  • 世の中にモデルを厚くしすぎる風潮がある原因は,一昔前までコントローラがテストしづらかったから。


なお上記の原則はRailsに限らず,他の言語やフレームワークでも同じである。

Web アプリの MVC 設計まとめ(CakePHP)
http://d.hatena.ne.jp/p4life/20091014/1255532618

1テーブルが1モデルに対応しているフレームワークでは…トランザクションやビジネスロジックがコントローラに混ざり込まないように注意。
それぞれのモデル単体をどれだけテストしたとしても、コントローラのテストをしなければアプリケーションの信頼性は上がりません。

対策として、モデルに収まりが悪いビジネスロジックは Service というモデルの形態に置くことをおすすめされています


 

*1:Exceleniumを使うとよいだろう。