keiino0425
@keiino0425 (いのぼー)

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

rspecのattributes_forで外部キーが取得できず困っています

解決したいこと

Railsでアプリを作り、そのテストを書いている時に詰まったことがあるので質問させていただきます。

FactoryBotのattributes_forで外部キーが取得できない問題です。

例)
Ruby on RailsでQiitaのようなWebアプリをつくっています。
記事を投稿する機能の実装中にエラーが発生しました。
解決方法を教えて下さい。

発生している問題・エラー

RspecのFactoryBotを利用しテストを作成しています。
仮予約モデル(temp_reservation)と予約モデル(reservation)にリレーションがあり、has_oneとbelong_toで関連付けをしています。

FactoryBotでもassociationで関連付けしています。

エラーが出る部分が

spec/controllers/reservations_controller.rb

  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で外部キーが取得できないために起こった問題だと推測しました。

ですが、どのようにすれば外部キーを取得できるのか、テストを正常に通過させることができるのか調べてみましたが解決には至らなかったためアドバイスをいただければ幸いです。

どうぞよろしくお願いいたします。

該当するソースコード

spec/factories/temp_reservations.rb
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

spec/factories/reservations.rb
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

spec/controllers/reservations_controller.rb
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


app/models/temp_reservation.rb
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

app/models/reservation.rb
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'

0

1Answer

attributes_forではassociationで関連付けられたオブジェクト(temp_reservation)までは生成されないのではないでしょうか。

以下のようにbuildを行った後にattributesでパラメータを生成するか、associationを使わずtemp_reservationをbuildする方式にすれば解決できると思います。

# ※id,created_at,updated_atなどは適宜除外してください
reservation_params = FactoryBot.build(:reservation, :user_reservation).attributes
1Like

Comments

  1. @keiino0425

    Questioner

    回答していただきありがとうございます!
    先にbuildしてパラメータ生成できるのですね!試していなかったです...

    上記のエラーは解決しました!ありがとうございます!

Your answer might help someone💌