Rspecでビヘイビア(振舞)駆動開発をしよう。でもテストの仕方がわからないとできませんね。今回は、コントローラーのテストでログインに挑戦です。
前提条件
- 「Rspecのインストールとテストの基本」ほぼ全て
- scaffoldでuser作成、Userモデルでのバリデート (「Rspecでモデルのテスト」)
- UsersControllerの変更 (「Rspecでコントローラーのテスト その1」)
ご自分で試してみたい方には以上の作業が必要です。
今回は以下のことを行います。
- Deviseをインストールし、ログインできるようにする
- Rspecでログインできるようにする
- 管理者がログインしてテストする
Deviseのインストールと設定
インストール
- Gemfile gem ‘devise’ を追加
- ターミナル(またはコマンドプロンプト)
1
2
3
$ cd rails_app
$ bundle install
$ rails generate devise:install
以上でインストールができました。
設定
続けて設定を行います。
ターミナル(またはコマンドプロンプト)
1
$ rails generate devise User
を実行することで config/routes.rb と app/models/user.rb に必要事項を挿入し、migrationファイルを用意してくれます。db:migrate します。
1
$ rake db:migrate
おっと、emailがぶつかってしまいました。
db/migrate/********_add_devise_to_users.rb のemailを設定する1行を削除して、もう一度db:migrateします。今度は成功しました。
各ファイルにそれぞれ記入
- config/environments/development.rb
1
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
- app/views/layouts/application.html.erb
1
2
<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
2
3
4
class ApplicationController < ActionController::Base
before_action :authenticate_user!
protect_from_forgery with: :exception
end
これで、ログインしなければ何もできなくなりました。rspec spec/controllers/user_controller_spec.rb を実行してみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[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 でログイン
Rspec でログインできるようになれば解決しそうです。まずRspecでdeviseのメソッドを使えるようにrails_helper.rbに以下の2行を追加します。
File: spec/rails_helper.rb
1
2
3
4
5
require 'devise'
RSpec.configure do |config|
config.include Devise::TestHelpers, :type => :controller
end
次に、spec/support/ の中に controller_macros.rb を作り以下を記述します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module ControllerMacros
def login_admin
before(:each) do
@request.env["devise.mapping"] = Devise.mappings[:admin]
admin = FactoryGirl.create(:admin, role: FactoryGirl.create(:role_admin))
sign_in admin
end
end
def login_user
before(:each) do
@request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryGirl.create(:user, role: FactoryGirl.create(:role_user))
sign_in user
end
end
end
このControllerMacrosを使うよう先ほどのrails_helper.rbに2行追加します。
1
2
3
4
5
6
7
require 'devise'
require 'support/controller_macros'
RSpec.configure do |config|
config.include Devise::TestHelpers, :type => :controller
config.extend ControllerMacros, :type => :controller
end
では、users_controller_spec.rb でログインしましょう。login_adminをRSpec.describe UsersController, type: :controller doの行の下に記入します。
1
2
3
4
5
6
7
8
9
10
require 'rails_helper'
RSpec.describe UsersController, type: :controller do
login_admin
let(:valid_attributes) {
FactoryGirl.attributes_for(:user, role: FactoryGirl.create(:role_user))
}
以下省略
rspec spec/controllers/user_controller_spec.rb を実行してみます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[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行目を編集します。
編集前
編集後
マッチャー inculdeを使いました。対象の配列や文字列の中にオブジェクトが含まれていることにマッチします。
Deviseの影響
属性passwordが更新されていないのでしょうか。実はDeviseはpasswordフィールドを使わずencrypted_passwordフィールドに値を暗号化して(暗号化はデフォルトでBcryptを使っています)格納します。そのためパスワードが正しいものであるかを確認するメソッドが用意されています。 valid_password?
です。このメソッドを使ってusers_controller_spec.rbを書きかえます。
1
expect(user.password).to eq("new_user_PASSWORD")
この93行目を次のように書き換えます。
1
2
3
4
5
6
7
8
9
it "リクエストされたユーザーを更新できる。" do
user = User.create! valid_attributes
put :update, {:id => user.to_param, :user => new_attributes}
user.reload
expect(user.name).to eq("new_user")
expect(user.valid_password?("new_user_PASSWORD")).to eq(true)
expect(user.email).to eq("new_user@example.com")
end
rspecを実行してみます。
1
2
3
4
5
6
[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の修正と複数スペックの実行
spec/models/user_spec.rb にも先ほどと同じuserのpassword属性に対する変更が必要です。
のようにテストしていた箇所を次のように変更します。
rspec spec/models/user_spec.rb を実行してみます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[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のバリデートをコメントにしておくことにします。
1
2
3
4
5
6
7
8
9
10
11
12
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
belongs_to :role
validates :role, presence: true
validates :name, length: {minimum: 2, maximum: 64, if: "name.present?"}, format: { with: /\A[^<>]*\z/ }, uniqueness: true, presence: true
#validates :email, presence: true
#validates :password, presence: true
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」から始まるように変更しました。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
it "一般ユーザが登録できる。" do
FactoryGirl.reload
FactoryGirl.create(:user, role: FactoryGirl.create(:role_user))
users = User.all
expect(users.size).to eq(1)
expect(users[0].name).to eq("User-1")
expect(users[0].role.role_name).to eq("general_user")
end
it "一般ユーザーが複数登録できる。" do
FactoryGirl.reload
user_role = FactoryGirl.create(:role_user)
FactoryGirl.create(:user, role: user_role)
FactoryGirl.create(:user, role: user_role)
FactoryGirl.create(:admin, role: FactoryGirl.create(:role_admin))
FactoryGirl.create(:user, role: user_role)
users = User.all
expect(users.size).to eq(4)
expect(users[0].name).to eq("User-1")
expect(users[1].name).to eq("User-2")
expect(users[3].name).to eq("User-3")
end
ハイライトした行が修正した箇所です。これで、
でも成功できました。
FactoryGirl.reload
は、FactoryGirl全体を初期化してしまうので大ナタを振るいすぎている気もしますが。。。
次回は、request spec に挑戦しようと思います。