動機
よく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に多少詳しくないと何を言っているかわからないかも…
コード
RSpec.configure do |config|
config.when_first_matching_example_defined(:require_login) do
require 'support/login'
end
end
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.rb
をrequire
するようになっています。
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_user
がnil
を返すようにしています。さらにexampleもここに書けるため、ログアウト時のテストケースをここに集約することができます。
また、ここで注目すべきはuser
がログインの対象であるため、各exampleでuser
を定義することでログインするユーザーの属性をコントロールできます。
メリット
- タグをつけるだけでユーザーがログインしている状態が自動的に作られる
- ログアウト時の挙動に関するexampleを一箇所で定義できる
- contextの不必要な増加が起きない
- RSpecの黒魔術を使いこなしている感があって楽しい😎
まとめ
上のコードをコピペすればだいたい動くはずです。
補足
:require_login
とか、名前がイケてないけど誰かもっといい命名あったら教えてください…