Posts Rspec でコントローラーのテスト その2
Post
Cancel

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

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

前提条件

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

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

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

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

Deviseのインストールと設定

インストール

  1. Gemfile gem ‘devise’ を追加
  2. ターミナル(またはコマンドプロンプト)
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 を作り以下を記述します。

File: 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行追加します。

File: spec/rails_helper.rb
>
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の行の下に記入します。

File: spec/controllers/users_controller_spec.rb
>
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行目を編集します。

編集前

>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
>
1
expect(user.password).to eq("new_user_PASSWORD")

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

File: spec/controllers/users_controller_spec.rb
>
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属性に対する変更が必要です。

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

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

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

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のバリデートをコメントにしておくことにします。

File: app/models/user.rb
>
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」から始まるように変更しました。

File: spec/controllers/users_controler_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
    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

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

>[rails_app]$ rspec spec/models spec/controllers

でも成功できました。

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

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

シェア
#内容発言者

Rspec でコントローラーのテスト その1

WordPress のプラグイン作成 はじめの一歩

坂井和郎