Rspec でコントローラーのテスト その2 Rspec Tips, Rails

modified at:08 March 2016

Rspecでビヘイビア(振舞)駆動開発をしよう。でもテストの仕方がわからないとできませんね。今回は、コントローラーのテストでログインに挑戦です。

前提条件

  1. 「Rspecのインストールとテストの基本」ほぼ全て
  2. scaffoldでuser作成、Userモデルでのバリデート (「Rspecでモデルのテスト」)
  3. UsersControllerの変更 (「Rspecでコントローラーのテスト その1」)

ご自分で試してみたい方には以上の作業が必要です。

今回は以下のことを行います。

  1. Deviseをインストールし、ログインできるようにする
  2. Rspecでログインできるようにする
  3. 管理者がログインしてテストする

Deviseのインストールと設定

インストール

  1. Gemfile gem ‘devise’ を追加
  2. ターミナル(またはコマンドプロンプト)
      $ cd rails_app
$ bundle install
$ rails generate devise:install
   

以上でインストールができました。

設定

続けて設定を行います。

ターミナル(またはコマンドプロンプト)

      $ rails generate devise User
   

を実行することで config/routes.rb と app/models/user.rb に必要事項を挿入し、migrationファイルを用意してくれます。db:migrate します。

      $ rake db:migrate
   

おっと、emailがぶつかってしまいました。

db/migrate/********_add_devise_to_users.rb のemailを設定する1行を削除して、もう一度db:migrateします。今度は成功しました。

各ファイルにそれぞれ記入

  • config/environments/development.rb
      config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
   
   
  • app/views/layouts/application.html.erb
      <p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
   
   

その他rootの設定とかもありますが、ここでは省略します(Devise で認証機能を追加などを参照してください)。

ApplicationController

app/controllers/application_controller.rb を編集して以下のようにします。

File: app/controllers/application_controller.rb
1 class ApplicationController < ActionController::Base
2   before_action :authenticate_user!
3   protect_from_forgery with: :exception
4 end

これで、ログインしなければ何もできなくなりました。rspec spec/controllers/user_controller_spec.rb を実行してみましょう。

      [denn@CentOS tdd_app]$ rspec spec/controllers/users_controller_spec.rb
FFFFFFFFFFFFFFFF

Failures:

  1) UsersController GET #index @users にすべてのユーザーを割り当てる。
     Failure/Error: get :index
     NoMethodError:
       undefined method `authenticate!' for nil:NilClass
     # ./spec/controllers/users_controller_spec.rb:16:in `block (3 levels) in <top (required)>'

        途中省略

Finished in 0.25626 seconds (files took 4.39 seconds to load)
16 examples, 16 failures

Failed examples:

rspec ./spec/controllers/users_controller_spec.rb:14 # UsersController GET #index @users にすべてのユーザーを割り当てる。

        途中省略

rspec ./spec/controllers/users_controller_spec.rb:131 # UsersController DELETE #destroy ユーザー一覧へリダイレクトする。
   

すべてがFになり、真っ赤です。全て失敗しました。

これらがすべて成功するように変更します。

Rspec でログインTopへ

Rspec でログインできるようになれば解決しそうです。まずRspecでdeviseのメソッドを使えるようにrails_helper.rbに以下の2行を追加します。

File: spec/rails_helper.rb
1 require 'devise'
2 
3 RSpec.configure do |config|
4   config.include Devise::TestHelpers, :type => :controller
5 end

次に、spec/support/ の中に controller_macros.rb を作り以下を記述します。

File: spec/support/controller_macros.rb
 1 module ControllerMacros
 2   def login_admin
 3     before(:each) do
 4       @request.env["devise.mapping"] = Devise.mappings[:admin]
 5       admin = FactoryGirl.create(:admin, role: FactoryGirl.create(:role_admin))
 6       sign_in admin
 7     end
 8   end
 9 
10   def login_user
11     before(:each) do
12       @request.env["devise.mapping"] = Devise.mappings[:user]
13       user = FactoryGirl.create(:user, role: FactoryGirl.create(:role_user))
14       sign_in user
15     end
16   end
17 end

このControllerMacrosを使うよう先ほどのrails_helper.rbに2行追加します。

File: spec/rails_helper.rb
1 require 'devise'
2 require 'support/controller_macros'
3 
4 RSpec.configure do |config|
5   config.include Devise::TestHelpers, :type => :controller
6   config.extend ControllerMacros, :type => :controller
7 end

では、users_controller_spec.rb でログインしましょう。login_adminをRSpec.describe UsersController, type: :controller doの行の下に記入します。

File: spec/controllers/users_controller_spec.rb
 1 require 'rails_helper'
 2 
 3 RSpec.describe UsersController, type: :controller do
 4   login_admin
 5 
 6   let(:valid_attributes) {
 7     FactoryGirl.attributes_for(:user, role: FactoryGirl.create(:role_user))
 8   }
 9 
10       以下省略

rspec spec/controllers/user_controller_spec.rb を実行してみます。

      [denn@CentOS tdd_app]$ rspec spec/controllers/users_controller_spec.rb
F........F......

Failures:

  1) UsersController GET #index @users にすべてのユーザーを割り当てる。
     Failure/Error: expect(assigns(:users)).to eq([user])

       expected: [#<User id: 2, name: "User-1", email: "user-1@example.com", password: nil, role_id: 1, created_at: "2015-06-11 03:06:03", updated_at: "2015-06-11 03:06:03", encrypted_password: "$2a$04$W3QkBGB8DD326rXfZF/ALOQy2Mz.rkhJBoXHFNhRDbK...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 0, current_sign_in_at: nil, last_sign_in_at: nil, current_sign_in_ip: nil, last_sign_in_ip: nil>]
            got: #<ActiveRecord::Relation [#<User id: 1, name: "Admin", email: "admin@example.com", password: nil, role_id: 2, created_at: "2015-06-11 03:06:02", updated_at: "2015-06-11 03:06:02", encrypted_password: "$2a$04$KsE94Zva4Fx2v/SzPHofAu3Seqf5b0vS47Rd1/lE8SD...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 0, current_sign_in_at: nil, last_sign_in_at: nil, current_sign_in_ip: nil, last_sign_in_ip: nil>, #<User id: 2, name: "User-1", email: "user-1@example.com", password: nil, role_id: 1, created_at: "2015-06-11 03:06:03", updated_at: "2015-06-11 03:06:03", encrypted_password: "$2a$04$W3QkBGB8DD326rXfZF/ALOQy2Mz.rkhJBoXHFNhRDbK...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 0, current_sign_in_at: nil, last_sign_in_at: nil, current_sign_in_ip: nil, last_sign_in_ip: nil>]>

       (compared using ==)

       Diff:
       @@ -1,2 +1,3 @@
       -[#<User id: 2, name: "User-1", email: "user-1@example.com", password: nil, role_id: 1, created_at: "2015-06-11 03:06:03", updated_at: "2015-06-11 03:06:03", encrypted_password: "$2a$04$W3QkBGB8DD326rXfZF/ALOQy2Mz.rkhJBoXHFNhRDbK...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 0, current_sign_in_at: nil, last_sign_in_at: nil, current_sign_in_ip: nil, last_sign_in_ip: nil>]
       +[#<User id: 1, name: "Admin", email: "admin@example.com", password: nil, role_id: 2, created_at: "2015-06-11 03:06:02", updated_at: "2015-06-11 03:06:02", encrypted_password: "$2a$04$KsE94Zva4Fx2v/SzPHofAu3Seqf5b0vS47Rd1/lE8SD...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 0, current_sign_in_at: nil, last_sign_in_at: nil, current_sign_in_ip: nil, last_sign_in_ip: nil>,
       + #<User id: 2, name: "User-1", email: "user-1@example.com", password: nil, role_id: 1, created_at: "2015-06-11 03:06:03", updated_at: "2015-06-11 03:06:03", encrypted_password: "$2a$04$W3QkBGB8DD326rXfZF/ALOQy2Mz.rkhJBoXHFNhRDbK...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 0, current_sign_in_at: nil, last_sign_in_at: nil, current_sign_in_ip: nil, last_sign_in_ip: nil>]

     # ./spec/controllers/users_controller_spec.rb:18:in `block (3 levels) in <top (required)>'

  2) UsersController PUT #update 正常な値の時 リクエストされたユーザーを更新できる。
     Failure/Error: expect(user.password).to eq("new_user_PASSWORD")

       expected: "new_user_PASSWORD"
            got: "PASSWORD_user-7"

       (compared using ==)
     # ./spec/controllers/users_controller_spec.rb:93:in `block (4 levels) in <top (required)>'

Finished in 0.41943 seconds (files took 1.61 seconds to load)
16 examples, 2 failures

Failed examples:

rspec ./spec/controllers/users_controller_spec.rb:15 # UsersController GET #index @users にすべてのユーザーを割り当てる。
rspec ./spec/controllers/users_controller_spec.rb:87 # UsersController PUT #update 正常な値の時 リクエストされたユーザーを更新できる。
   

2個の失敗がありましたが、それ以外はログインできたことで成功しています。失敗の原因を突き止めて解決していきましょう。

ログインの影響

失敗のメッセージを見ると期待していた値が「User-1」一人だけだったのに対して受っとった値は「User-1」とログインで作られた「Admin」の二人になったことが原因です。spec/controllers/users_controller_spec.rbの18行目を編集します。

編集前

      expect(assigns(:users)).to eq([user])
   

編集後

      expect(assigns(:users)).to include(user)
   

マッチャー inculdeを使いました。対象の配列や文字列の中にオブジェクトが含まれていることにマッチします。

Deviseの影響

属性passwordが更新されていないのでしょうか。実はDeviseはpasswordフィールドを使わずencrypted_passwordフィールドに値を暗号化して(暗号化はデフォルトでBcryptを使っています)格納します。そのためパスワードが正しいものであるかを確認するメソッドが用意されています。 valid_password? です。このメソッドを使ってusers_controller_spec.rbを書きかえます。

File: spec/controllers/users_controller_spec.rb
93 expect(user.password).to eq("new_user_PASSWORD")

この93行目を次のように書き換えます。

File: spec/controllers/users_controller_spec.rb
87 it "リクエストされたユーザーを更新できる。" do
88   user = User.create! valid_attributes
89   put :update, {:id => user.to_param, :user => new_attributes}
90   user.reload
91 
92   expect(user.name).to eq("new_user")
93   expect(user.valid_password?("new_user_PASSWORD")).to eq(true)
94   expect(user.email).to eq("new_user@example.com")
95 end

rspecを実行してみます。

      [rails_app]$ rspec spec/controllers/users_controller_spec.rb
................

Finished in 0.40239 seconds (files took 1.62 seconds to load)
16 examples, 0 failures
   

すべて成功しました。

user_spec.rbの修正と複数スペックの実行Topへ

spec/models/user_spec.rb にも先ほどと同じuserのpassword属性に対する変更が必要です。

            expect(user[0].password).to eq("PASSWORD")
   

のようにテストしていた箇所を次のように変更します。

            expect(user[0].valid_password?("PASSWORD")).to eq(true)
   

rspec spec/models/user_spec.rb を実行してみます。

      [rails_app]$ rspec spec/models/user_spec.rb
......FF....

Failures:

  1) User バリデート:  emailは必須である。
     Failure/Error: expect(user.errors[:email]).to eq([I18n.t('errors.messages.blank')])

       expected: ["can't be blank"]
            got: ["can't be blank", "can't be blank"]

       (compared using ==)
     # ./spec/models/user_spec.rb:83:in `block (3 levels) in <top (required)>'

  2) User バリデート:  passwordは必須である。
     Failure/Error: expect(user.errors[:password]).to eq([I18n.t('errors.messages.blank')])

       expected: ["can't be blank"]
            got: ["can't be blank", "can't be blank"]

       (compared using ==)
     # ./spec/models/user_spec.rb:88:in `block (3 levels) in <top (required)>'

Finished in 0.21613 seconds (files took 1.61 seconds to load)
12 examples, 2 failures

Failed examples:

rspec ./spec/models/user_spec.rb:80 # User バリデート:  emailは必須である。
rspec ./spec/models/user_spec.rb:85 # User バリデート:  passwordは必須である。
   

email と passward について同じエラーメッセージが2つずつ取得されています。modelでバリデートした結果とdeviseがバリデートした結果のものと思われるので、ここではmodelのバリデートをコメントにしておくことにします。

File: app/models/user.rb
 1 class User < ActiveRecord::Base
 2   # Include default devise modules. Others available are:
 3   # :confirmable, :lockable, :timeoutable and :omniauthable
 4   devise :database_authenticatable, :registerable,
 5          :recoverable, :rememberable, :trackable, :validatable
 6   belongs_to :role
 7 
 8   validates :role, presence: true
 9   validates :name, length: {minimum: 2, maximum: 64, if: "name.present?"}, format: { with: /\A[^<>]*\z/ }, uniqueness: true, presence: true
10   #validates :email, presence: true
11   #validates :password, presence: true
12 end

devise の設定が追加されていますが、ここでは詳しくは触れません。デフォルトの設定のままです。

以上で、

  • spec/models/roles_spec.rb
  • spec/models/users_spec.rb
  • spec/controllers/users_controller_spec.rb

がすべて成功するようになったと思います。

が、単独で各スペックを実行するときには成功しますが、users_spec.rb と users_controller_spec.rb を合わせてテストすると:userファクトリーで使ったsequenceが連続した値を使うため期待した値にならないことがわかりました。そこで、コントローラーで:userファクトリーを使うときに、sequenceの値をリセット( FactoryGirl.reload )して「1」から始まるように変更しました。

File: spec/controllers/users_controler_spec.rb
 1     it "一般ユーザが登録できる。" do
 2       FactoryGirl.reload
 3       FactoryGirl.create(:user, role: FactoryGirl.create(:role_user))
 4 
 5       users = User.all
 6       expect(users.size).to eq(1)
 7       expect(users[0].name).to eq("User-1")
 8       expect(users[0].role.role_name).to eq("general_user")
 9     end
10 
11     it "一般ユーザーが複数登録できる。" do
12       FactoryGirl.reload
13       user_role = FactoryGirl.create(:role_user)
14       FactoryGirl.create(:user, role: user_role)
15       FactoryGirl.create(:user, role: user_role)
16       FactoryGirl.create(:admin, role: FactoryGirl.create(:role_admin))
17       FactoryGirl.create(:user, role: user_role)
18 
19       users = User.all
20       expect(users.size).to eq(4)
21       expect(users[0].name).to eq("User-1")
22       expect(users[1].name).to eq("User-2")
23       expect(users[3].name).to eq("User-3")
24     end

ハイライトした行が修正した箇所です。これで、

      [rails_app]$ rspec spec/models spec/controllers
   

でも成功できました。

FactoryGirl.reload は、FactoryGirl全体を初期化してしまうので大ナタを振るいすぎている気もしますが。。。

次回は、request spec に挑戦しようと思います。

  created at:14 June 2015




[投稿する]場合は、 枠の中の図形をマウスでドラッグして、(あるいは指で)なぞって下さい。それほど厳密でなくても大丈夫です。