rspecのattributes_forで外部キーが取得できず困っています
解決したいこと
Railsでアプリを作り、そのテストを書いている時に詰まったことがあるので質問させていただきます。
FactoryBotのattributes_forで外部キーが取得できない問題です。
例)
Ruby on RailsでQiitaのようなWebアプリをつくっています。
記事を投稿する機能の実装中にエラーが発生しました。
解決方法を教えて下さい。
発生している問題・エラー
RspecのFactoryBotを利用しテストを作成しています。
仮予約モデル(temp_reservation)と予約モデル(reservation)にリレーションがあり、has_oneとbelong_toで関連付けをしています。
FactoryBotでもassociationで関連付けしています。
エラーが出る部分が
略
describe "#create" do
context "as an authenticated user" do
before do
@user = FactoryBot.create(:user)
end
context "with valid attributes" do
it "adds a reservation" do
reservation_params = FactoryBot.attributes_for(:reservation, :user_reservation)
sign_in @user
expect {
post :create, params: { reservation: reservation_params }
}.to change(@user.reservations, :count).by(1)
end
end
context "with invalid attributes" do
it "does not add a project" do
reservation_params = FactoryBot.attributes_for(:reservation, :user_reservation, :invalid)
sign_in @user
expect {
post :create, params: { reservation: reservation_params }
}.to_not change(@user.reservations, :count).by(1)
end
end
end
context "as a guest" do
it "returns a 302 response" do
reservation_params = FactoryBot.attributes_for(:reservation, :user_reservation)
post :create, params: { reservation: reservation_params }
expect(response).to have_http_status "302"
end
it "redirects to the sign-in page" do
reservation_params = FactoryBot.attributes_for(:reservation, :user_reservation)
post :create, params: { reservation: reservation_params }
expect(response).to redirect_to "/users/sign_in"
end
end
end
の部分で、
reservation_params = FactoryBot.attributes_for(:reservation, :user_reservation)
とすると
NoMethodError: undefined method `start_time' for nil:NilClass
というエラーメッセージが表示されます。
binding.pryをして原因を探すと、FactoryBot.attributes_for(:reservation, :user_reservation)でtemp_reservationの外部キーを取得できていないようでした。
[1] pry(#<RSpec::ExampleGroups::Reservation>)> FactoryBot.attributes_for(:reservation)
=> {:start_time=>Wed, 22 Feb 2023 10:00:00.000000000 JST +09:00,
:end_time=>Wed, 22 Feb 2023 11:30:00.000000000 JST +09:00,
:address_select=>true}
traitを指定せずにattributes_forをすると問題なく取得することができます。
仮予約がない予約も存在するようなシステムなため、optional:trueを設定しており、外部キーを取得する必要がないため問題なく取得できるのだと思います。
[2] pry(#<RSpec::ExampleGroups::Reservation>)> a = FactoryBot.build(:reservation, :user_reservation)
=> #<Reservation:0x0000aaaad9095c70
id: nil,
user_id: 236,
teacher_id: 283,
start_time: Wed, 22 Feb 2023 10:00:00.000000000 JST +09:00,
created_at: nil,
updated_at: nil,
end_time: Wed, 22 Feb 2023 11:30:00.000000000 JST +09:00,
address_select: true,
temp_reservation_id: nil>
[3] pry(#<RSpec::ExampleGroups::Reservation>)> a = FactoryBot.build(:reservation)
=> #<Reservation:0x0000aaaad8e0eb28
id: nil,
user_id: nil,
teacher_id: nil,
start_time: Wed, 22 Feb 2023 10:00:00.000000000 JST +09:00,
created_at: nil,
updated_at: nil,
end_time: Wed, 22 Feb 2023 11:30:00.000000000 JST +09:00,
address_select: true,
temp_reservation_id: nil>
buildは問題なく行えるので、attributes_forで外部キーが取得できないために起こった問題だと推測しました。
ですが、どのようにすれば外部キーを取得できるのか、テストを正常に通過させることができるのか調べてみましたが解決には至らなかったためアドバイスをいただければ幸いです。
どうぞよろしくお願いいたします。
該当するソースコード
FactoryBot.define do
time = Time.zone.now.beginning_of_day + 1.day + 10.hour
later_time = time + 90.minutes
week_time = time + 6.day
week_later_time = week_time + 90.minutes
yesterday_time = time - 2.day
yesterday_later_time = yesterday_time + 90.minutes
today_time = time - 1.day
today_later_time = today_time + 90.minutes
month_time = today_time + 1.month
month_later_time = month_time + 90.minutes
factory :temp_reservation do
association :user, strategy: :create
association :teacher, strategy: :create
start_time { time }
end_time { later_time }
address_select { true }
#開始時間が1週間以内
trait :start_week do
start_time { week_time }
end_time { week_later_time }
end
#開始時間が昨日以降
trait :start_yesterday do
start_time { yesterday_time }
end_time { yesterday_later_time }
end
#開始時間が今日
trait :start_today do
start_time { today_time }
end_time { today_later_time }
end
#開始時間が1ヶ月先以降
trait :start_month do
start_time { month_time }
end_time { month_later_time }
end
end
end
FactoryBot.define do
time = Time.zone.now.beginning_of_day + 1.day + 10.hour
later_time = time + 90.minutes
today_time = Time.zone.now.beginning_of_day + 10.hour
three_months_time = today_time + 3.month
three_months_later_time = three_months_time + 90.minutes
factory :reservation do
association :teacher
start_time { time }
end_time { later_time }
address_select { true }
#生徒の予約
trait :user_reservation do
association :temp_reservation
user { temp_reservation.user }
teacher { temp_reservation.teacher }
start_time { temp_reservation.start_time }
end_time { temp_reservation.end_time }
address_select { temp_reservation.address_select }
end
#開始時間が昨日以降
trait :start_yesterday do
association :temp_reservation, :start_yesterday
user { temp_reservation.user }
teacher { temp_reservation.teacher }
start_time { temp_reservation.start_time }
end_time { temp_reservation.end_time }
address_select { temp_reservation.address_select }
end
#開始時間が今日
trait :start_today do
association :temp_reservation, :start_today
user { temp_reservation.user }
teacher { temp_reservation.teacher }
start_time { temp_reservation.start_time }
end_time { temp_reservation.end_time }
address_select { temp_reservation.address_select }
end
# 開始時間が3ヶ月先以降
trait :start_three_months do
start_time { three_months_time }
end_time { three_months_later_time }
end
end
end
require 'rails_helper'
RSpec.describe ReservationsController, type: :controller do
describe "#index" do
before do
@user = FactoryBot.create(:user)
@teacher = FactoryBot.create(:teacher)
end
# 認証済みのユーザーとして
context "as an authenticated user" do
# 正常にレスポンスを返すこと
it "responds successfully" do
sign_in @user
get :index, params: { user_id: @user.id, teacher_id: @teacher.id }
expect(response).to be_successful
end
# 200レスポンスを返すこと
it "returns a 200 response" do
sign_in @user
get :index, params: { user_id: @user.id, teacher_id: @teacher.id }
expect(response).to have_http_status "200"
end
end
# ゲストとして
context "as a guest" do
# 302レスポンスを返すこと
it "returns a 302 response" do
get :index, params: { user_id: @user.id, teacher_id: @teacher.id }
expect(response).to have_http_status "302"
end
# サインイン画面にリダイレクトすること
it "redirects to the sign-in page" do
get :index, params: { user_id: @user.id, teacher_id: @teacher.id }
expect(response).to redirect_to "/users/sign_in"
end
end
end
describe "#teacher_index" do
before do
@teacher = FactoryBot.create(:teacher)
end
# 認証済みの講師として
context "as an authenticated teacher" do
# 正常にレスポンスを返すこと
it "responds successfully" do
sign_in @teacher
get :teacher_index, params: { teacher_id: @teacher.id }
expect(response).to be_successful
end
# 200レスポンスを返すこと
it "returns a 200 response" do
sign_in @teacher
get :teacher_index, params: { teacher_id: @teacher.id }
expect(response).to have_http_status "200"
end
end
# ゲストとして
context "as a guest" do
# 302レスポンスを返すこと
it "returns a 302 response" do
get :teacher_index, params: { teacher_id: @teacher.id }
expect(response).to have_http_status "302"
end
# サインイン画面にリダイレクトすること
it "redirects to the sign-in page" do
get :teacher_index, params: { teacher_id: @teacher.id }
expect(response).to redirect_to "/teachers/sign_in"
end
end
end
describe "#create" do
context "as an authenticated user" do
before do
@user = FactoryBot.create(:user)
end
context "with valid attributes" do
it "adds a reservation" do
reservation_params = FactoryBot.attributes_for(:reservation, :user_reservation)
sign_in @user
expect {
post :create, params: { reservation: reservation_params }
}.to change(@user.reservations, :count).by(1)
end
end
context "with invalid attributes" do
it "does not add a project" do
reservation_params = FactoryBot.attributes_for(:reservation, :user_reservation, :invalid)
sign_in @user
expect {
post :create, params: { reservation: reservation_params }
}.to_not change(@user.reservations, :count).by(1)
end
end
end
context "as a guest" do
it "returns a 302 response" do
reservation_params = FactoryBot.attributes_for(:reservation, :user_reservation)
post :create, params: { reservation: reservation_params }
expect(response).to have_http_status "302"
end
it "redirects to the sign-in page" do
reservation_params = FactoryBot.attributes_for(:reservation, :user_reservation)
post :create, params: { reservation: reservation_params }
expect(response).to redirect_to "/users/sign_in"
end
end
end
end
class TempReservation < ApplicationRecord
belongs_to :user, optional: true
belongs_to :teacher
has_one :reservation
validates :user_id, :teacher_id, :start_time, :end_time, :address_select, presence: true
def self.check_reservation_day(user_id, start_time)
same_user_temp_reservation = TempReservation.where(user_id: user_id)
same_user_reservation = Reservation.where(user_id: user_id)
temp_start = same_user_temp_reservation.map(&:start_time)
start = same_user_reservation.map(&:start_time)
temp_start.each do |i|
if start_time < (i + 8.days)
return "仮予約との間隔が1週間以内のため予約できません。"
end
end
start.each do |i|
if start_time < (i + 8.days)
return "予約との間隔が1週間以内のため予約できません。"
end
end
if start_time < Date.current
return "過去の日付は選択できません。正しい日付を選択してください。"
elsif start_time < (Date.current + 1)
return "当日は選択できません。正しい日付を選択してください。"
elsif (Date.current >> 1) < start_time
return "1ヶ月以降の日付は選択できません。正しい日付を選択してください。"
end
end
end
class Reservation < ApplicationRecord
belongs_to :user, optional: true
belongs_to :teacher
belongs_to :temp_reservation, optional: true
validates :teacher_id, :start_time, :end_time, :address_select, presence: true
def self.reservations_after_three_month
# 今日から3ヶ月先までのデータを取得
reservations = Reservation.all.where("start_time >= ?", Date.current).where("start_time < ?", Date.current >> 3).order(start_time: :desc)
# 配列を作成し、データを格納
# DBアクセスを減らすために必要なデータを配列にデータを突っ込んでます
reservation_data = []
reservations.each do |reservation|
reservations_hash = {}
reservations_hash.merge!(start_time: reservation.start_time, end_time: reservation.end_time)
reservation_data.push(reservations_hash)
end
reservation_data
end
def self.check_reservation_day(start_time)
if start_time < Date.current
return "過去の日付は選択できません。正しい日付を選択してください。"
elsif start_time < (Date.current + 1)
return "当日は選択できません。正しい日付を選択してください。"
elsif (Date.current >> 3) < start_time
return "3ヶ月以降の日付は選択できません。正しい日付を選択してください。"
end
end
def self.check_delete(start_time)
day_before = (start_time - 1.days).to_date
s = day_before.strftime("%Y-%m-%d") + " 18:00:00"
deadline = Time.zone.parse(s)
if deadline <= Time.zone.now
return "受講前日の18時以降の予約取消はキャンセル料が発生致します。恐れ入りますがJSMC事務局までキャンセルのご連絡をお願い致します。"
end
end
end
補足情報(FW/ツールのバージョンなど)
ruby '2.7.5'
rails '6.1.6'