17
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Tama.rbAdvent Calendar 2018

Day 12

RSpecでログイン処理をDRYに書きたい

Posted at

動機

よくRailsでbefore_action :authenticate!みたいなコードを書いて、それをRSpecでテストしたいときがあります。
これをDRYに書くのは結構難しい。

問題

普通にべた書きするとだいたいこうなります。

describe 'My API' do
  context 'when user is logging in' do
    let(:user) { create(:user) }
    before { allow_any_instance_of(ApplicationController).to receive(:current_user) { user }
    # API Spec...
  end
  
  context 'when user is not logging in' do
    # Specs for user logout...
  end
end

# これを各Specにコピペする…

つらいのは以下の2点です。

contextが増える

ユーザーがログインしているのがほぼ必須なAPIを実装している場合でも、ユーザーがログインしていない場合の挙動をテストしようと思うとユーザーがログアウトしているケースのためのcontextを増やさなくてはならない。

コピペが増える

ログアウト時のcontextは各Spec間で共有できないので、コピペするしかない。

解決策

RSpecのwhen_first_matching_example_definedとshared_contextの組み合わせで劇的に改善します。
まずはコードを掲載します。解説は続きますが、RSpecに多少詳しくないと何を言っているかわからないかも…

コード

spec/rails_helper.rb
RSpec.configure do |config|
  config.when_first_matching_example_defined(:require_login) do
    require 'support/login'
  end
end
spec/support/login.rb
def login_as(user)
  allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)
end

RSpec.shared_context 'when login required' do
  let(:user) { create(:user) }
  before do
    login_as(user)
  end

  context 'when current_user is nil' do
    before do
      allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(nil)
    end

    it 'depends on your spec' do
      # actual spec goes here...
    end
  end
end

RSpec.configure do |config|
  config.include_context 'when login required', :require_login
end

解説

rails_helper.rbから見ていきます。
when_first_matching_example_definedはexampleを実行していって引数のタグを最初に発見した際にブロックを実行してくれるようなものです。
ここでは:require_loginというタグを発見するとsupport/login.rbrequireするようになっています。
support/login.rb内では、最下部でinclude_contextを行っています。ここで第2引数を:require_loginと、先程when_first_matching_example_definedで設定したものと同じシンボルを指定します。第1引数は任意の文字列ですが、こちらも次に指定する文字列と同じにします。
RSpec.shared_context 'when login required' doという行はshared_contextを定義しています。ここで引数の文字列を先程指定した文字列と同じにすることで、:require_loginのタグを与えたすべてのexampleにshared_context内のcontextが挿入されます。
shared_context内を見てみると、まずユーザーのログイン処理に相当することを行った上で、ログアウト時に相当するcontextを別に作ってcurrent_usernilを返すようにしています。さらにexampleもここに書けるため、ログアウト時のテストケースをここに集約することができます。
また、ここで注目すべきはuserがログインの対象であるため、各exampleでuserを定義することでログインするユーザーの属性をコントロールできます。

メリット

  • タグをつけるだけでユーザーがログインしている状態が自動的に作られる
  • ログアウト時の挙動に関するexampleを一箇所で定義できる
  • contextの不必要な増加が起きない
  • RSpecの黒魔術を使いこなしている感があって楽しい😎

まとめ

上のコードをコピペすればだいたい動くはずです。

補足

:require_loginとか、名前がイケてないけど誰かもっといい命名あったら教えてください…

17
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?