Railsのコントローラをテストする:Railsで目指せ、情熱エンジニア(9)
前回はインテグレーションテストとしてCucumberでテストを作成しました。今回はユニットテストとして、RSpecを使ってコントローラのテストを作成します
前回はインテグレーションテストとしてCucumberでテストを作成しました。今回はユニットテストとしてRSpecを使ってコントローラのテスト(RSpecのテストコードは“スペック”と呼ばれるので、以降はスペックと呼びます)を作成します。本稿で紹介するスペックの全文はGitHub上にあります。
最初に、コードレビューの回で述べたコントローラの役割についてもう一度おさらいしてみましょう。
コントローラは外部から来たリクエストを受け付け、レスポンスを返すのが役割です。具体的には以下の3つの動作をおこないます。
- 適切なオブジェクトをとってくる
- オブジェクトに対する何らかの操作を指示する
- 操作が成功した際と失敗した際のビューの振る舞いを指定する
重要なのが、コントローラが何から何まで処理を行うのではなく、いろいろなモデルからオブジェクトを呼んできて「こういう処理をしてください」とお願いすることに徹することです。そうしないと他のコントローラで似たような処理をするときに、複数のコントローラにロジックを重複して書かなければいけないからです。
これらがちゃんと行われているのを確認するためのテストを書くのが今回の目的です。現在のコントローラには上記以外のロジックもたくさん含まれているのですが、コントローラテストを書いていくことで「あれ、このロジックはコントローラでやるべきじゃないよね。モデル層に移すことができるかちょっと試してみよう」と考えていくことが大切です。コントローラテストやモデルテストは自分のコードをデザインするためのツールとして考えるようになると、「テストを先に書く」というTDDや「テストを仕様書として考える」というBDDの考えや習慣が徐々に身に付いていくのではないでしょうか。
まず、何はともあれスペックの全文を掲載します。
require 'spec_helper' describe ItemsController do include Devise::TestHelpers # to give your spec access to helpers before(:each) do @user = FactoryGirl.create(:user) @params = { :user_id => @user.id, :item => {:url => 'http://www.google.com'} } end describe "#create" do describe "when successful" do before(:each) do sign_in :user, @user end it "should create new item" do expect{ post :create, @params }.to change(Item, :count).by(1) end it "should redirect to edit page" do post :create, @params response.should redirect_to edit_user_item_path(@user, Item.last) end it "should assign variables" do post :create, @params flash[:notice].should == "Created an item. Any changes?" assigns[:user].should == @user assigns[:item].should_not be_nil end end describe "when non author accessed" do before(:each) do different_user = FactoryGirl.create(:user) sign_in :user, different_user end it "should not create new item" do expect{ post :create, @params }.to change(Item, :count).by(0) end it "should redirect to users page" do post :create, @params response.should redirect_to users_path end end describe "when invalid url is passed" do before(:each) do sign_in :user, @user @params[:item] = {:url => 'wrongurl'} end it "should not create new item" do expect{ post :create, @params }.to change(Item, :count).by(0) end it "should redirect to users page" do post :create, @params response.should redirect_to user_recent_path(@user.username) end it "should assign variables" do post :create, @params flash[:error].should == "Invalid URL!!" end end describe "when times out" do before(:each) do Timeout.should_receive(:timeout).and_raise(Timeout::Error) sign_in :user, @user end it "should not create new item" do expect{ post :create, @params }.to change(Item, :count).by(0) end it "should redirect to users page" do post :create, @params response.should redirect_to user_recent_path(@user.username) end it "should assign variables" do post :create, @params flash[:error].should == "Timeout! Could not retrieve data from the URL!!" end end describe "setting item" do before(:each) do @item = FactoryGirl.create(:item) sign_in :user, @user end %w{edit destroy update}.each do |action| it "#{action} should have item" do get action.to_sym, @params.merge(:id => @item.id ) assigns[:item].should_not be_nil end end end end end
では、上記のスペックの中から重要な部分を順に見て行きましょう。まずは、冒頭の1行。
include Devise::TestHelpers
これは認証機能を提供するDeviseのモジュールです。このモジュールをインクルードすることで、singn_in、sign_outといったログイン、ログアウトをテスト内でシミュレートするためのいくつかのメソッドを使うことができるようになります。次に、
before(:each) do
ですが、これは各describeブロックが走る前に必要な処理を記述する箇所です。共通の前処理をまとめておく場所です。
テストデータからオブジェクを作る「Factory Girl」
続いて、以下の行を見てみましょう。
@user = FactoryGirl.create(:user)
ここではFactory Girlというライブラリを使用してテストユーザーを作成しています。RailsにはFixture(フィクスチャ)というテストデータを以下のようなYAMLファイルで作成し、各テストで共有する仕組みが提供されています。
one: url: MyString bitly_url: MyString title: MyString subtitle: MyString published_at: 2010-10-26 summary: MyText hatena: 1 retweet: 1 private_memo: MyText user: two: url: MyString bitly_url: MyString title: MyString subtitle: MyString published_at: 2010-10-26 summary: MyText hatena: 1 retweet: 1 private_memo: MyText user: @user = Factory(:user)
以前は私もこのFixtureの仕組みを使っていたのですが、何十にも渡るテストケースに合致するデータを作るのは結構大変な上に、テストコードを読むときに、テストファイルとFixtureのファイルを行ったり来たりする必要があるので結構使いづらいです。
そこで「テストデータの雛形ファイルは用意するけれど、実際にはテストの度に必要なオブジェクトを作成する」という方式が取られるようになってきました。そのためのツールとしてはFactory GirlとFixture Replacementが有名です。今回はFactory Girlを使用します。
まずは、spec/factories/item_factory.rbに以下のような雛形を作成します。
FactoryGirl.define do factory :user do sequence(:email) {|n| "bob#{n}@example.com" } sequence(:username) {|n| "bob#{n}" } password "password" invite_code "dummy_code" end end
「password "password"」というところは、「passwordコラムに"password"という文字を入れる」ということです。emailとusernameには「sequence」が使われています。これはvalidates_uniqunessなどのバリデーションが設定されているときに、同じユーザー名のオブジェクトを複数作成されるとエラーが出るのを避けるためです。これによって「bob1」「bob2」といったように毎回違うユーザ名を作成することができます。
FacotryGirl.build(:user) を呼ぶと、新しいUserオブジェクトを作成しますが、データベースには保存しません。データベースに依存しないことをテストするとき(例えばvalidates_presence_ofなど)には、こちらを使った方がデータベース書き込みの時間を節約でき、テスト時間を少し短縮することが可能です。
FactoryGirl.create(:user)あるいはFactoryGirl(:user)はどちらともデータベースにオブジェクトを保存します。
例えばパスワードの値の有無を調べるときに以下のようにすることで、雛形に指定された値を上書きすることができます。
FactoryGirl.create(:user, :password => nil)
これは以下と全く同じですが、自分がテストするときに関係する箇所だけをFactoryに指定してあげるだけで、テストがぐっと読みやすくなるのではないでしょうか。
User.create(:email => "bob@example.com", :username => "bob", :password => nil, :invite_code "dummy_code")
expectを使って状態変化をスマートに記述
それでは最初のテスト項目です。ここでは「いくつかのパラメータをcreateメソッドに渡した結果itemオブジェクトが作られる」ということを意味しています。
it "should create new item" do expect{ post :create, @params }.to change(Item, :count).by(1) end
「expect」のブロックの中でテストする処理を行い、その処理が行なった結果起きる変化を「change」内で記述しています。
これは以下のテストコードと全く同じなのですが、上の方が読みやすくないですか? テストを簡潔に記述するための、こういった仕組みが随所に含まれているのもRSpecの魅力でしょう。
it "should create new item" do item_count_before = Item.count post :create, @params item_count_after = Item.count (item_count_after - item_count_before).should == 1 end
リダイレクトなどの挙動もテストできる「response」
「response」もrspecがコントローラテストのために提供してるオブジェクトで、各メソッドを読んだ後「レスポンスはこういう動きをするべきだ」といったExpectation(期待/予想)を含んでいます。今回のスペックでは以下のように使っています。
it "should redirect to edit page" do post :create, @params response.should redirect_to edit_user_item_path(@user, Item.last) end
responseでは主に以下のような期待値を指定することができます。今回の例ではedit_user_item_pathにリダイレクトするかどうかをテストしています。
response.should be_success # 200を返す response.should be_redirect # 30xを返す response.should render_template("path/to/template/for/action") # ビューを描画する response.should have_text("expected text") # レスポンスボディにある文字列を含む
インスタンス変数やFlashメッセージをテストする
次に、以下のテストコードを見てみましょう。
it "should assign variables" do post :create, @params flash[:notice].should == "Created an item. Any changes?" assigns[:user].should == @user assigns[:item].should_not be_nil end
ここでは「1.適切なオブジェクトをとってくる」の部分をテストしています。「assigns[:key]」の「key」の部分にインスタンス変数名を指定します。インスタンス変数以外にもflashやsessionの値をテストすることもできます。
assigns[:item].should_not be_nil
この部分は「@item変数に何か入っている」かをテストしているのですが、この書き方には賛否両論があるかもしれません。なぜなら「@item = "foo"」のような値が入った場合でもテストが通ってしまうからです。そこで、もう少し具体的に、
assigns[:item].url should == 'http://www.google.com'
と、期待値を指定する方が良いかもしれません。ただ、厳格にしすぎると、今度はちょっとしたコードの変更でも修正しなければならないテストコードが増えてしまって困りものです。「どのテストがどういった振る舞いに対して責任を持つか」ということを常に意識しながら緩いテストを書くか、厳格なテストを書くかを使い分けると良いです。
今回はコントローラテストをRspecで作成しました。テストもコードなので、重複した部分を1つの変数にまとめたり、FactoryやRSpecの便利な機能を利用してどんどん可読性を高めるようにしていきましょう。また今回は特に触れていませんが、成功したときのテストを書くだけでなく、エラーケースのテストもどんどん付け加えていきましょう。テストがしっかりしていれば、後でコードのリファクタリングをしても、何かおかしい変更があればテストがフェイルすることで警告してくれます。テストコードは、そういう変化を受け止めてくれるためのセーフティネットの役割を果たしてくれるはずです。
次回はコントローラのコードを見ていきましょう。
Copyright © ITmedia, Inc. All Rights Reserved.