request spec とは
controller spec との違いが最初はわかりづらいかもしれませんが、 controller spec は PostsController
などの単体テストを書くためのもので、以下のような特徴があります。
-
get :index
などと書くが、第一引数はアクション名であり、実際の url とはまったく関係がない。 -
get :show, id: 3
のようにして、params[:id]
に値を直接渡すことができる。 -
post :create, {}, {user_id: 5}
のようにして、session[:user_id]
に値を直接渡すことができる。 - デフォルトでは、 view ファイルの中身は評価されない。 (
render_template
マッチャでどの view ファイルが render されるかをテスト出来るが、 中身は評価されないので partial の読み込みまでは確認出来ない。この挙動はrender_views
宣言によって変更可能) -
spec/controllers/
以下に置くか、もしくはtype: :controller
宣言をする。
一方、 request spec は、実際にブラウザの挙動をシミュレートするのに近いです。
-
get '/users/2'
などと、実際の path を書く必要がある。 -
params
やsession
に値を渡すことができないので、サインインなどの手続きをそのままする必要がある。 -
spec/requests/
以下に置くか、もしくはtype: :request
宣言をする。
マルチセッションの書き方
以下のようなテストを書くことは出来るのでしょうか?
- ユーザAが post を編集中にユーザBが同じ post に編集を加えて先に保存
- ユーザAがユーザBの post にコメントした後、ユーザBがサインインすると通知を受け取る
- ユーザAがルームを作成し、ユーザBがそこにアクセスすると、ユーザAの画面にユーザBの名前が表示される
はい、出来ますってことで、Rack::Test::Methods
を include してやると、 with_session
ていう便利メソッドが使えるようになるのでそれでごりごり書けばおk。
with_session
の引数は単なる名前なので何でもよくて、同じ名前を指定すれば同じセッションの続きを書くことが出来る。
rails_helper.rb
RSpec.configure do |config|
config.include Rack::Test::Methods, type: :request
end
require 'rails_helper'
RSpec.describe 'multi session', type: :request do
let(:user1){ FactoryGirl.create(:user) }
let(:user2){ FactoryGirl.create(:user) }
it do
with_session(:taro) do
post '/sign_in', user_name: user1.name, password: user1.password
post '/rooms', name: "taro's room"
json ||= JSON.parse(last_response.body)
@room_id = json['id']
end
with_session(:jiro) do
post '/sign_in', user_name: user2.name, password: user2.password
post "/rooms/#{@room_id}/join"
end
with_session(:taro) do
get "/rooms/#{@room_id}"
json ||= JSON.parse(last_response.body)
expect(json['members'].include?(user2.name)
end
end
end
最初からこのコードだけ見せればこれ以上説明いらない感じですね。
おまけ
宣伝がてらで、私が趣味で作ってる 人狼アリーナのテストコードを晒しておきます。 twitter oauth をモックしたりしているのでご参考ください。
rails_helper.rb
RSpec.configure do |config|
config.include MultiSession, type: :request
end
spec/support/multi_session.rb
module MultiSession
include Rack::Test::Methods
def login_twitter
OmniAuth.config.test_mode = true
uid = FactoryGirl.generate(:uid)
OmniAuth.config.mock_auth[:twitter] = {
provider: 'twitter',
uid: uid,
info: {name: "twitter_#{uid}"},
extra: { raw_info: {} }
}.with_indifferent_access
get "/s/auth/twitter/callback"
Auth.find_by(uid: uid.to_s).user
end
def json
@json ||= JSON.parse(last_response.body)
end
end
spec/requests/wolf_spec.rb
require 'rails_helper'
describe Creature::Wolf do
let(:village){ FactoryGirl.create(:village, :playing, days_count: 2, regulation:{wolf: 1, possessed: 1, body_guard: 1, seer: 1}) }
let(:wolf){ village.creatures_a.type(:wolf)[0] }
let(:possessed){ village.creatures_a.type(:possessed)[0] }
let(:body_guard){ village.creatures_a.type(:body_guard)[0] }
let(:seer){ village.creatures_a.type(:seer)[0] }
before do
with_session(:possessed) { possessed.update_column(:user_id, login_twitter.id) }
with_session(:body_guard) { body_guard.update_column(:user_id, login_twitter.id) }
with_session(:seer) { seer.update_column(:user_id, login_twitter.id) }
with_session(:wolf) { wolf.update_column(:user_id, login_twitter.id) }
end
describe '襲撃' do
def update_actions(hash)
patch "/s/villages/#{village.id}/days/1.json", {actions: hash}
end
before do
with_session(:possessed) { update_actions(vote: body_guard.id) }
with_session(:body_guard) { update_actions(vote: possessed.id, guard: seer.id) }
with_session(:seer) { update_actions(vote: possessed.id, see: wolf.id) }
with_session(:wolf) { update_actions(vote: possessed.id, bite: bite_id) }
end
context '投票も護衛もされていない狩人を襲撃' do
let(:bite_id){ body_guard.id }
it '勝利する' do
with_session(:wolf) do
expect(json['village']['days'][1]['victim_ids']).to eq [body_guard.id]
expect(json['village']['days'][1]['notices'][wolf.id.to_s]['bite']).to eq body_guard.id
expect(json['village']['winner_side']).to eq 'wolf'
expect(json['messages']['epilogue']).to include ['winner_side', '人狼が勝利しました。', '人狼が勝利しました。']
end
end
end
context '投票対象を襲撃' do
let(:bite_id){ possessed.id }
it '失敗する' do
with_session(:wolf) do
expect(json['village']['days'][1]['victim_ids']).to eq []
expect(json['village']['days'][1]['notices'][wolf.id.to_s]['bite']).to eq 'fail'
end
end
end
context '護衛対象を襲撃' do
let(:bite_id){ seer.id }
it '失敗する' do
with_session(:wolf) do
expect(json['village']['days'][1]['victim_ids']).to eq []
expect(json['village']['days'][1]['notices'][wolf.id.to_s]['bite']).to eq 'fail'
end
end
end
end
end