はじめに
コントローラーの単体テストやRSpecの利用方法を勉強中です。
メモも兼ねてQiitaに投稿しました。
実行環境
- rails 6.0.0
- ruby 2.5.6
- RSpec 4.0.0
RSpecのインストール
テストコードはRSpec,Factorybotを利用するため、以下をインストール
gem 'rspec-rails', '~> 4.0.0'
gem 'factory_bot_rails'
コントローラーの役割
テストコードを作成するにあたって、コントローラーの役割を一度振り返る。
コントローラー内のアクションがリクエストを受け取ったあと処理(データが必要であればモデル等と連携)を行い、レスポンスを返す。
なので、以下をベースにテストコードを記述します。
- 受信したリクエストに対して適切なレスポンスを返す
- データ処理が必要な場合はモデルと連携する
- レスポンスを表示するのに適切なビューやリダイレクト先を選択する
テストコード記載
本記事ではGroupsというコントローラーのテストコードを記述します。
事前準備
FactoryBot作成
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name {'testuser'}
provider {'cognito-idp'}
uid {'1111aaa-00zz-1111-0000-1a1a1a1a1a1a'}
nickname {'testuser'}
trait :user_with_groups do
after(:build) do |user|
user.groups << create(:group)
end
end
end
end
必要なカラムを記述。
アソシエーション先のGroupテーブルを纏めて作成する場合は、
trait :user_with_groups
を利用します。
# spec/factories/groups.rb
FactoryBot.define do
factory :group do
name {'testgroup'}
summary {'テスト用グループ'}
trait :group_with_users do
after(:build) do |group|
group.users << create(:user)
end
end
trait :invalid do
name {''}
end
trait :updategroup do
name {'updategroup'}
end
end
end
:invalid
と:updategroup
はcreateやupdateアクション時に利用します。
Before
# spec/factories/groups_spec.rb
RSpec.describe "Groups", type: :request do
let(:user) { FactoryBot.create(:user, :user_with_groups) }
before do
allow_any_instance_of(ActionDispatch::Request).to receive(:session).and_return({ user_id: user.id })
end
Groupsコントローラーはsign_inが前提となるため、ユーザーを作成。
:user_with_groups
を指定して、関連するグループも作成しておきます。
allow_any_instance_of
でダミーのsessionメソッドを作成します。
GET #index
# spec/factories/groups_spec.rb
describe 'GET #index' do
before do
get groups_path
end
it 'indexアクションにリクエストすると正常にレスポンスが返ってくる' do
expect(response.status).to eq(200)
end
it 'index.html.erbにユーザー名が表示される' do
expect(response.body).to include 'testuser'
end
end
HTTPステータスコードと、ビューファイルにユーザー名が表示されているかを確認
GET #new
# spec/factories/groups_spec.rb
describe 'GET #new' do
before do
get new_group_path
end
it 'newアクションにリクエストすると正常にレスポンスが返ってくる' do
expect(response.status).to eq(200)
end
end
indexと同様、HTTPステータスコードを確認
POST #create
# spec/factories/groups_spec.rb
describe 'POST #create' do
context '有効なパラメーターの場合' do
let(:group_params) { FactoryBot.attributes_for(:group, :group_with_users) }
it 'createアクションにリクエストすると正常にリダイレクトされる' do
post groups_path, params: { group: group_params }
expect(response.status).to eq(302)
end
it 'テーブルに新しいデータが登録される' do
expect{
post groups_path, params: { group: group_params }
}.to change(Group, :count).by(1)
end
it 'indexアクションにリダイレクトされる' do
post groups_path, params: { group: group_params }
expect(response).to redirect_to(groups_path)
end
end
context '無効なパラメーターの場合' do
let(:group_params) { FactoryBot.attributes_for(:group, :group_with_users, :invalid) }
it 'createアクションにリクエストすると正常にレスポンスが返ってくる' do
post groups_path, params: { group: group_params }
expect(response.status).to eq 200
end
it 'テーブルに新しいデータが登録されない' do
expect{
post groups_path, params: { group: group_params }
}.to change(Group, :count).by(0)
end
it 'new.html.erbにエラーが表示される' do
post groups_path, params: { group: group_params }
expect(response.body).to include 'can't be blank'
end
end
end
HTTPステータス、テーブルの更新有無、リダイレクト先、エラーの表示を確認。
FactoryBot.attributes_for
でハッシュパラメーターを生成。
無効なパラメーターの場合は、trait
で定義した:invalid
を呼び出してnameを空で作成。
GET #show
# spec/factories/groups_spec.rb
describe 'GET #show' do
before do
get group_path(group_id)
end
context '参加しているグループが指定された場合' do
let(:group_id) { user.groups.ids[0] }
it 'showアクションにリクエストすると正常にレスポンスが返ってくる' do
expect(response.status).to eq(200)
end
it 'show.html.erbにグループ名が表示される' do
expect(response.body).to include 'testgroup'
end
end
context '参加していないグループが指定された場合' do
let(:group_id) { 2 }
it 'showアクションにリクエストすると正常にリダイレクトされる' do
expect(response.status).to eq(302)
end
it 'indexアクションにリダイレクトされる' do
expect(response).to redirect_to(groups_path)
end
end
context '存在しないグループが指定された場合' do
let(:group_id) { 999999 }
it 'showアクションにリクエストすると正常にリダイレクトされる' do
expect(response.status).to eq(302)
end
it 'indexアクションにリダイレクトされる' do
expect(response).to redirect_to(groups_path)
end
end
end
HTTPステータス、グループ名の表示、リダイレクト先を確認。
contextに応じてgroup_idを変えています。
user.groups.ids[0]
はgroups_spec.rbの冒頭で関連するテーブルを作成済なので、idを代入してます。
存在しないグループが指定された場合のケースは、ActiveRecord::RecordNotFound
の例外処理で、groups_pathへリダイレクトする様にしているので、それを想定したテストとなります。
GET #edit
# spec/factories/groups_spec.rb
describe 'GET #edit' do
before do
get edit_group_path(group_id)
end
context '参加しているグループが指定された場合' do
let(:group_id) { user.groups.ids[0] }
it 'editアクションにリクエストすると正常にレスポンスが返ってくる' do
expect(response.status).to eq(200)
end
it 'edit.html.erbにユーザー名が表示される' do
expect(response.body).to include 'testuser'
end
it 'edit.html.erbにグループ名が表示される' do
expect(response.body).to include 'testgroup'
end
end
context '参加していないグループが指定された場合' do
# 省略
end
context '存在しないグループが指定された場合' do
# 省略
end
end
他のアクション同様、HTTPステータス、ユーザー名、グループ名の表示、リダイレクト先を確認。
# 省略
部分はshowアクションと同様です。
PUT #update
# spec/factories/groups_spec.rb
describe 'PUT #update' do
context '有効なパラメーターの場合' do
let(:group_id) { user.groups.ids[0] }
it 'updateアクションにリクエストすると正常にリダイレクトされる' do
put group_path(group_id), params: { group: FactoryBot.attributes_for(:group, :updategroup) }
expect(response.status).to eq(302)
end
it 'テーブルが更新される' do
expect{
put group_path(group_id), params: { group: FactoryBot.attributes_for(:group, :updategroup) }
}.to change { Group.find(group_id).name }.from('testgroup').to('updategroup')
end
it 'showアクションにリダイレクトされる' do
put group_path(group_id), params: { group: FactoryBot.attributes_for(:group, :updategroup) }
expect(response).to redirect_to(group_path(group_id))
end
end
context '無効なパラメーターの場合' do
let(:group_id) { user.groups.ids[0] }
it 'updateアクションにリクエストすると正常にレスポンスが返ってくる' do
put group_path(group_id), params: { group: FactoryBot.attributes_for(:group, :invalid) }
expect(response.status).to eq(200)
end
it 'テーブルが更新されない' do
expect{
put group_path(group_id), params: { group: FactoryBot.attributes_for(:group, :invalid) }
}.to_not change(Group.find(group_id), :name)
end
it 'edit.html.erbにエラーが表示される' do
put group_path(group_id), params: { group: FactoryBot.attributes_for(:group, :invalid) }
expect(response.body).to include 'can't be blank'
end
end
context '参加していないグループが指定された場合' do
# 省略
end
context '存在しないグループが指定された場合' do
# 省略
end
end
createアクションとほぼ同じで、HTTPステータス、テーブルの更新有無、リダイレクト先、エラーの表示を確認。
contextに応じて:updategroup
と:invalid
を利用してハッシュパラメーターを作成し、リクエストしてます。
# 省略
部分はshow,editアクションと同様です。
要確認
- allow_any_instance_of(ActionDispatch::Request).to receive(:session).and_return({ user_id: user.id })
- allow_any_instance_ofの利用は非推奨らしい。
- SessionsHelper内で作成したsign_inメソッドを利用する想定でしたが、sessionのNoMethoderrorがでてしまったため、このやり方に行きつきました。
- 他の方法がないか、継続調査。
- destroyのテスト
- 今回は削除の要件がなく実施しておりませんが、必要なタイミングで調べたい。
- createとupdateで一部冗長な記述があるのでリファクタリングする。
参考記事
https://qiita.com/blueberrystream/items/884d0d23caf50b6553a0
https://qiita.com/jnchito/items/42193d066bd61c740612
https://qiita.com/t2kojima/items/ad7a8ade9e7a99fb4384