FunctionTest から RSpec on Rails に書き換えてみた (コントローラ編)
前回の続きです、今回は FunctionTest から RSpec on Rails に書き変えてみました。
シンプルな置き換え
まずは、単純に置き換えてみました。
# todos_controller_test.rb require 'test_helper' class TodosControllerTest < ActionController::TestCase def setup @request.session[:login] = true end def test_should_get_index get :index assert_response :success assert_not_nil assigns(:todos) end def test_should_show_todo get :show, :id => todos(:task1).id assert_response :success assert_not_nil assigns(:todo) end def test_not_login @request.session[:login] = nil get :index assert_redirected_to({:controller => 'login', :action => 'index'}, "ログインせずにindexをアクセスするとログインページにログインページにリダイレクトする") end end
describe 〜 end はネストできるのでアクション毎に describe を書いてみました。RSpec版も FunctionTest版とほぼ同じような感じです。
# todos_controller_spec.rb require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe TodosController do fixtures :todos, :users describe "indexをGETしたら" do before(:each) do session[:login] = true get :index end it "成功する" do response.should be_success end it "@todosにデータが入る" do assigns[:todos].should_not be_empty end end describe "showをid=:task3でGETしたら" do before(:each) do session[:login] = true get :show, :id => todos(:task3) end it "成功する" do response.should be_success end it "@todoにデータが入る" do assigns[:todo].should_not be_nil end end describe "ログインせずにindexをGETしたら" do before(:each) do @request.session[:login] = nil get :index end it "ログインページにログインページにリダイレクトする" do response.should redirect_to(:controller => 'login', :action => 'index') end end end
実行結果は
TodosController indexをGETしたら - 成功する - @todosにデータが入る TodosController showをid=:task3でGETしたら - 成功する - @todoにデータが入る TodosController ログインせずにindexをGETしたら - ログインページにログインページにリダイレクトする Finished in 0.196078 seconds 5 examples, 0 failures
モック、スタブを使ってみる
RSpec は簡単にモック(Mock)、スタブ(stub)をコードを書かずに簡単に使えるようになっています。とてもRubyらしいメタプログラミングですね!
モック、スタブを使うとモデル(Model)のAPIをスタブで置き換えてしまうとコントローラーだけの/だけでテストが書けます。
例えば
Todo.stub!(:find).with(:all, anything()).and_return([@todo1, @todo2])
は、Todo.find(:all, 任意の引数) が呼び出されると、[@todo1, @todo2] を戻す と定義しています。
また、
Todo.should_receive(:find).with("3").and_return(@todo3)
は、Todo.find("3")が呼び出される事をチェックしています、その際に戻り値は@todo3になります。
モックを使った RSpec は以下のようになりました。
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe TodosController do describe "indexをGETしたら" do before(:each) do @todo1 = mock_model(Todo) @todo2 = mock_model(Todo) Todo.stub!(:find).with(:all, anything()).and_return([@todo1, @todo2]) session[:login] = true get :index end it "成功する" do response.should be_success end it "todosにデータが入る" do assigns[:todos].should eql([@todo1, @todo2]) end end describe "showをid=3でGETしたら" do before(:each) do @todo3 = mock_model(Todo) Todo.should_receive(:find).with("3").and_return(@todo3) session[:login] = true get :show, :id =>3 end it "成功する" do response.should be_success end it "find(3)が実行される" do end it "@todoにid=3のデータが入る" do assigns[:todo].should equal(@todo3) end end describe "ログインせずにindexをGETしたら" do before(:each) do @request.session[:login] = nil get :index end it "ログインページにログインページにリダイレクトする" do response.should redirect_to(:controller => 'login', :action => 'index') end end end
考察
モック、スタブを使うとコントローラだけで/だけのテストを実行できますが、モデルのAPIをモック・スタブで定義するのでテストコードはコントローラのコードを意識したホワイトボックステストになってしまいます。したがって
- メリットとして
- モデルが出来てなくてもコントローラが作成できる
- モデルのAPIが明確にできる
- デメリットとして
- モック・スタブ定義でテストコードが増える
- コントローラのコード変更は、テストコードに直結する
- モック・スタブで定義をするにはかなりコードを考えて(書いて)からでないと難しい
Railsの場合コントローラ、モデルともにコードは少ないので、モック、スタブを積極的に使ってテストを書くのはコストがかかりすぎ、Railsの軽快な開発スタイルには必ずしも向かないのではと思いました?
そこで、実際にRSpecを使っている方に質問してみました。回答は、
- コントローラだけで/だけのテストを実行できるのは魅力だがモック、スタブを積極的に使うとコードの変更にRSpecの変更が付いて行けず本末転倒になってしまった経験がある
- ロジックはモデルに置き、コントローラはなるべくシンプルにしコントローラのRSpec(テスト)は減らす
- また、自動生成されたコードにRSpec(テスト)を書くような事は避ける
- コントローラのテストは cucumber やselenium にまかせては?
などでした。
続きは、Rails勉強会@東京第36回 で・・・・