0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails初心者が勘違いしやすいFactoryBotの設計 Strong ParametersではなくModel・Schemaを見る理由

0
Posted at

はじめに

この記事は、RailsでFactoryBotを作成する際に学んだ内容を、自分用の備忘録としてまとめたものです。

テストコードを作成する際にFactoryを意識せずに使用していたのでこの期に何となくではなく理解できるように改めておさらいしました。

予約管理機能のFactoryを作成する中で、モデルのバリデーションだけでなく、DBスキーマや関連モデルも確認する必要があると分かりました。


Factoryを作る前に確認するもの

Factoryを作るときは、主に次の順番で確認します。

schema.rb・マイグレーション
        ↓
モデル
        ↓
関連するFactory
        ↓
作成対象のFactory

それぞれの役割は次のとおりです。

DBスキーマ
→ NULL制約、外部キー、CHECK制約、初期値

モデル
→ バリデーション、関連、業務ルール

Factory
→ 有効なテストデータの初期状態

FactoryBotは、Controllerを経由せず、モデルのインスタンスを直接生成します。

create(:reservation)

イメージとしては、次の処理に近いです。

reservation = Reservation.new(...)
reservation.save!

そのため、Factory自体を作る際の基準はStrong Parametersではなく、モデルとDB制約です。


schema.rbから必須項目を確認する

今回の reservations テーブルには、次のように定義しています。

t.bigint "created_by_staff_id", null: false
t.datetime "ends_at", null: false
t.integer "guest_count", null: false
t.bigint "requested_restaurant_master_type_id", null: false
t.string "reservation_name", limit: 50, null: false
t.string "reservation_phone_number", limit: 20, null: false
t.bigint "reservation_status_id", null: false
t.datetime "starts_at", null: false
t.bigint "updated_by_staff_id", null: false

null: false が設定されているため、Factoryには最低限、次の属性や関連が必要です。

reservation_name
reservation_phone_number
starts_at
ends_at
guest_count
requested_restaurant_master_type
reservation_status
created_by_staff
updated_by_staff

一方、次の値は通常、Factoryに書く必要がありません。

created_at
updated_at
lock_version

created_atupdated_at はRailsが設定し、lock_version にはDB側の初期値が設定されているためです。


任意項目は必要になるまで書かない

null: false がなく、モデル側でも必須ではない属性は、通常のFactoryから省略できます。

例えば、次のような属性です。

customer
reservation_route
menu_type
occasion
allergy_note
request_note
internal_memo
canceled_at

モデルやDBに初期値がなければ、省略した属性は通常 nil になります。

そのため、次のようにすべて明示する必要はありません。

customer { nil }
request_note { nil }
internal_memo { nil }

Factoryは、基本的に「保存可能な最小限の状態」にします。

テストで必要な値は、そのテスト側から上書きできます。

create(
  :reservation,
  customer: customer,
  request_note: "窓側希望"
)

任意項目をすべてFactoryへ書くと、カラム追加や仕様変更のたびに確認対象が増えてしまいます。


DBのCHECK制約も確認する

今回のテーブルには、次のCHECK制約がありました。

t.check_constraint "ends_at > starts_at"
t.check_constraint "guest_count > 0"
t.check_constraint "lock_version >= 0"

そのため、Factoryも次の条件を満たす必要があります。

終了日時は開始日時より後
予約人数は1人以上
lock_versionは0以上

当初は、開始日時と終了日時に同じ値を設定していました。

starts_at { "2026-07-01 18:00" }
ends_at   { "2026-07-01 18:00" }

しかし、これでは次の制約に違反します。

ends_at > starts_at

モデルのバリデーションを通過しても、DB制約に違反すれば保存できません。


属性同士の関係をFactoryで表す

開始日時と終了日時は、次のように定義しました。

starts_at do
  Time.zone.local(
    Time.zone.today.year,
    Time.zone.today.month,
    Time.zone.today.day,
    18,
    0
  )
end

ends_at { starts_at + 2.hours }

ends_at が同じFactory内の starts_at を参照しているため、開始日時を変更しても終了日時との関係を維持できます。

reservation = build(
  :reservation,
  starts_at: Time.zone.local(2026, 7, 10, 12, 0)
)

reservation.ends_at
# => 2026-07-10 14:00:00

開始日時と終了日時を別々の固定値にするよりも、DB制約を満たしやすくなります。


sequenceで値の重複を避ける

FactoryBotの sequence を使うと、連番を含む値を生成できます。

sequence(:reservation_name) do |number|
  "予約者#{number}"
end

生成結果は次のようになります。

予約者1
予約者2
予約者3

電話番号も同様です。

sequence(:reservation_phone_number) do |number|
  format("090%08d", number)
end
09000000001
09000000002
09000000003

ただし、sequence はテストデータの重複を避けるための機能です。

本番データの一意性を保証するには、モデルのバリデーションだけでなく、DBのユニークインデックスも必要です。


associationで関連データを作る

FactoryBotでは、association を使って関連モデルを生成できます。

association :created_by_staff,
            factory: :staff

予約を作成するときに、関連するStaffも作成されます。

reservation = create(:reservation)

reservation.created_by_staff
# => Staffのインスタンス

作成者と更新者が同じ担当者でよい場合は、次のように同じインスタンスを使用できます。

association :created_by_staff,
            factory: :staff

updated_by_staff { created_by_staff }

両方を個別の association にすると、予約1件に対してStaffが2人作られます。

別担当者が必要なテストでは、テスト側から上書きします。

create(
  :reservation,
  created_by_staff: creator,
  updated_by_staff: updater
)

traitで状態の違いを表す

キャンセル済みなどの状態は、trait で表現できます。

trait :canceled do
  canceled_at { Time.current }
end

trait :details_confirmed do
  details_confirmed_at { Time.current }
end

通常の予約は次のように作成します。

create(:reservation)

キャンセル済みの場合は、traitを指定します。

create(:reservation, :canceled)

複数のtraitを組み合わせることもできます。

create(
  :reservation,
  :canceled,
  :details_confirmed
)

通常状態に不要な値を基本Factoryへ書かず、状態ごとの差分だけをtraitへ分けられます。


スキーマだけではFactoryは完成しない

スキーマを見れば、DB上の必須項目や制約は分かります。

しかし、関連モデルの業務ルールまでは分かりません。

例えば、次の2つは同じ standard_list_masters テーブルを参照しているとします。

association :requested_restaurant_master_type,
            factory: :standard_list_master

association :reservation_status,
            factory: :standard_list_master

モデル側で次のような制約がある場合、単純な standard_list_master では保存できない可能性があります。

requested_restaurant_master_type
→ 店舗種別に属する選択肢のみ許可

reservation_status
→ 予約ステータスに属する選択肢のみ許可

そのため、次の内容まで確認します。

schema.rb
→ DB制約

Reservationモデル
→ バリデーションと関連

StandardListMasterモデル
→ 基本コードの業務ルール

既存Factory
→ 有効な関連データを作れるか

関連Factoryが未整備の場合は、テスト側で有効な値を渡します。

create(
  :reservation,
  requested_restaurant_master_type: table_type,
  reservation_status: confirmed_status,
  created_by_staff: staff,
  updated_by_staff: staff
)

同じ指定を多くのテストで繰り返す場合は、用途別の専用Factoryを作ると管理しやすくなります。


Reservation Factoryの基本形

今回の内容を反映した基本形は次のとおりです。

FactoryBot.define do
  factory :reservation do
    sequence(:reservation_name) do |number|
      "予約者#{number}"
    end

    sequence(:reservation_phone_number) do |number|
      format("090%08d", number)
    end

    starts_at do
      Time.zone.local(
        Time.zone.today.year,
        Time.zone.today.month,
        Time.zone.today.day,
        18,
        0
      )
    end

    ends_at { starts_at + 2.hours }

    guest_count { 2 }

    association :requested_restaurant_master_type,
                factory: :standard_list_master

    association :reservation_status,
                factory: :standard_list_master

    association :created_by_staff,
                factory: :staff

    updated_by_staff { created_by_staff }

    trait :canceled do
      canceled_at { Time.current }
    end

    trait :details_confirmed do
      details_confirmed_at { Time.current }
    end
  end
end

このFactoryは、スキーマ上の必須項目とCHECK制約を満たしています。

ただし、次の関連はモデルの業務ルールに合っているか、別途確認が必要です。

requested_restaurant_master_type
reservation_status

Strong ParametersはFactoryの基準ではない

Strong Parametersは、HTTPリクエストから受け取った値のうち、モデルへの一括代入を許可する属性を制限する仕組みです。

params.require(:reservation).permit(
  :reservation_name,
  :starts_at,
  :ends_at
)

一方、次のFactoryBotの処理はControllerを通りません。

create(:reservation)

そのため、Factoryを作る際にStrong Parametersを基準にする必要はありません。

ただし、Request Specでは実際にControllerを経由するため、Strong Parametersも確認対象になります。

Factoryを作るとき
→ schema.rb、モデル、関連Factory

Request Specを書くとき
→ 上記に加えて、ルーティング、Controller、Strong Parameters

まとめ

FactoryBotを作るときは、次の点を確認します。

  • null: false やDBの初期値を確認する
  • CHECK制約を満たす値を設定する
  • 任意項目は必要になるまで書かない
  • sequence で重複しにくい値を作る
  • association で関連データを作る
  • 状態の違いは trait で表現する
  • スキーマだけでなくモデルの業務ルールも確認する
  • Strong ParametersはFactoryではなく、HTTPリクエスト側の制御として考える

FactoryBotは、Model SpecだけでなくRequest SpecやSystem Specでも使用するテストデータの土台です。

今後はモデルだけを見てFactoryを作るのではなく、DB制約と関連モデルまで確認したうえで、「保存可能な最小限の状態」を定義していきます。


参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?