rspec-rails (2.14.0) + devise (3.2.1, 3.2.0) + capybara (2.2.0) でsign in判定

deviseを使っていて、rspec内でサインインしているかどうかの判定をしたいときのやり方。

バージョンごとに微妙に変化があるようで、実際に動作確認したのはタイトルのもののみです。
他のバージョンに関しては、この記事では対応出来ない可能性があるため、注意してください。


1つずつ注意点を確認して行きます。
最終的な結論は記事の下の方にコードをおいておくので、やり方だけ分かれば良いという人は下までスキップしてください。

devise

deviseを使う際に問題になる事の1つに、rspecでのsign in、sign outの検証が、deviseが提供する’sign_in’と’sign_out'メソッドでは出来ないという事があります。

そこで、以下のページで解決策が提示されています。

https://github.com/plataformatec/devise/wiki/How-To:-Test-with-Capybara

このページではまず、Capybaraによって提供されるメソッドを使って、Sign inフォームを直接埋めることでsign inする事を推奨しています。しかし推奨した後に、この方法ではテストの数が増えた時に時間がかかってしまうとも言っています。

そこで、warden test helpersというヘルパーを用いてこの問題を解決する方法が書かれています。

そして、なぜ’sign_in’と’sign_out’メソッドを使わないかというと、以下のような理由らしいです。

If you're wondering why we can't just use Devise's built in sign_in and sign_out methods, it's because these require direct access to the request object which is not available while using Capybara. To bundle the functionality of both methods together you can create a helper method.

翻訳(適当)すると
『なぜDeviseによって提供されている’sign_in’と’sign_out’メソッドを使わないのか疑問に思うだろう。その理由は、Capybaraを使っている時には使う事のできない”request object”に直接アクセスする必要があるから。なので、それら2つのメソッドを持っているヘルパーメソッドを自分で作ってやる必要がある。』
らしいです。

という事で、deviseの問題はwarden test helpersで解決する事が出来ます。

capybara

rails_tutorial(http://railstutorial.jp/?version=4.0#top)をやっていると、capybaraを使用しているspecが spec/requests というフォルダ内にある事に気がつくかもしれません。
そうして、capybara v2.2.0でも同じようにするとうまくいかなくてはまります。
2.2.0では仕様変更があったらしく、capybaraが動作するのは <b>spec/features</b> 内のファイルに対してとなっています。
ここを間違えていると、いくらdeviseの問題を解決してもテストが通りません。

また、Capybaraを使用するためには、rspecに Capybara::DSL をincludeする必要もあります。

結論

以上2つの問題を解決するよう定義したspec_helperが以下になります。

#spec/spec_helper.rb

# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'rspec/autorun'

# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

# Checks for pending migrations before tests are run.
# If you are not using ActiveRecord, you can remove this line.
ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration)

RSpec.configure do |config|
  # ## Mock Framework
  #
  # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
  #
  # config.mock_with :mocha
  # config.mock_with :flexmock
  # config.mock_with :rr

  # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
  config.fixture_path = "#{::Rails.root}/spec/fixtures"

  # If you're not using ActiveRecord, or you'd prefer not to run each of your
  # examples within a transaction, remove the following line or assign false
  # instead of true.
  config.use_transactional_fixtures = true

  # If true, the base class of anonymous controllers will be inferred
  # automatically. This will be the default behavior in future versions of
  # rspec-rails.
  config.infer_base_class_for_anonymous_controllers = false

  # Run specs in random order to surface order dependencies. If you find an
  # order dependency and want to debug it, you can fix the order by providing
  # the seed, which is printed after each run.
  #     --seed 1234
  config.order = "random"

  # for Capybara : Capybara用のDSLをinclude
  config.include Capybara::DSL

  # for Devise : Deviseを使用している時にsign in/outを取るためのヘルパーをinclude
  include Warden::Test::Helpers
end 

さらに、capybaraは必ず spec/features フォルダー内で使用してください。
そうしないとvisitやpageが定義されていません、というようなエラーが出て、テストが赤くなります。


最後に、実際にログイン、ログアウトの処理の仕方を確認します。

login_as(user, :scope => :user)     #ユーザーをadmin falseでログイン
logout(:user)     #ログインしているユーザーをログアウトさせる

上記2つのメソッドで、ログイン、ログアウトの処理を実行します。
下にサンプルのspecファイルをおいておいたので、照らし合わせて確認してみてください。

#spec/features/application_controller_spec.rb  !!features 直下であることに注意
require 'spec_helper'
 
describe 'ApplicationController' do
 
  let(:user) { FactoryGirl.create(:user) }
 
  describe 'header' do
    
    describe 'has menus' do
 
      before { visit root_path }
 
      specify 'whenever a user is signed in or not' do
        expect(page).to have_link('Home', root_path)
      end
 
      specify 'when a user is signed in' do
        login_as(user, :scope => :user)
        visit root_path
 
        expect(page).to have_link('Sign out', destroy_user_session_path)
        expect(page).not_to have_link('Sign up', new_user_registration_path)
        expect(page).not_to have_link('Sign in', new_user_session_path)
      end
 
      specify 'when a user is signed in' do
        logout(:user)  
      
        expect(page).to have_link('Sign up', new_user_registration_path)
        expect(page).to have_link('Sign in', new_user_session_path)
        expect(page).not_to have_link('Sign out', destroy_user_session_path)
     end
    end
  end
end

rspec-rails (2.14.0) + devise (3.2.1, 3.2.0) + cap ...