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(テスト)を書くような事は避ける
  • コントローラのテストは cucumberselenium にまかせては?

などでした。


続きは、Rails勉強会@東京第36回 で・・・・