RSpecを結構書いてますが、テストを書く上で、FactoryBotが整備されていると楽ですよね。
ただ、最低限でも過剰でも、テストケースが漏れたりします。
また、無駄にINSERTされて、テストが遅くなったりする事もあります。
改めて整理する為に、こう書くのが良いだろうと思うベストプラクティス!?をまとめてみました。

単体で作成できるように作る

これは基本だと思います。rails cで、一時的に作りたい場合にも役立ちます。

必須項目は全て埋める。できるだけユニークな値となるように

Request Specで値の検証をする時に、複数レコードで全て同じ値だと異なるレコードの値を表示している可能性が残ってしまいます。
また、バリデーションが通らない場合もあります。

spec/factories/users.rb

FactoryBot.define do
  factory :user do
    code               { Digest::MD5.hexdigest(SecureRandom.uuid).to_i(16).to_s(36).rjust(25, '0') }
    sequence(:name)    { |n| "user(#{n})" }
    email              { Faker::Internet.email(name: "#{name}#{Faker::Number.hexadecimal(digits: 3)}") }
    password           { Faker::Internet.password(min_length: 8) }

codeとemailはユニーク。SecureRandom.uuidはユニーク(一意)な値が取れます。
nameは重複しても問題ないけど、sequenceで異なるように設定。
Faker::Internet.emailは毎回違う値が取れるけど、ユニークになる事を期待しない方が良いので、nameを指定。ただ、rails cを起動しなおするとsequenceは1からになるので、Faker::Number.hexadecimalでランダムな値もセットするようにしています。

※ユニークはsequenceやSecureRandom.uuidで。Fakerと組み合わせるのも良いけど、Fakerだけでユニークになる事を期待してはいけない。

必須のリレーションはassociationで作成する

バリデーションが通らないので、associationで作成します。

spec/factories/spaces.rb

FactoryBot.define do
  factory :space do
    sequence(:code)        { |n| Faker::Number.hexadecimal(digits: 3) + n.to_s.rjust(5, '0') }
    sequence(:name)        { |n| "space(#{n})" }
    sequence(:description) { |n| "description(#{n})" }
    private                { true }
    association :created_user, factory: :user

associationで:created_user(モデルのbelongs_to)に:userのfactoryを割り当てています。

任意のリレーションは、実行速度(無駄にINSERTが走る)以外に、紐付けが無い(リレーション先がなくて例外になる)ケースにも対応できるように、ここでは作成しない方は良い。
紐付けが必要なケースでは、traitを作っても良いけど、Specで明示的にセットしてあげれば充分なケースが多い。
下記はシンプル過ぎるけど、last_updated_userを先に作って、spaceをcontext毎に作る場合は、last_updated_userの作成が1回で済むので速度的に有利。

let_it_be(:last_updated_user) { FactoryBot.create(:user) }
let_it_be(:space) { FactoryBot.create(:space, last_updated_user:) }

※factory直下で、任意のリレーションは作らない方が良い。

任意項目もできるだけ埋める

Request Specで値の検証をする時に任意項目がnilだと、テストの意味が無くなる。
Specで全ての値をセットしても良いけど、一覧と詳細等、複数箇所で定義するのは辛い。
任意項目は数が多い場合も多いので、怠いけど、先にやっておいた方が幸せになれる。

バリデーションが通らなかったり、ケース的に存在しない組み合わせはnilでも良い。
次に記載するtraitで対応する。

spec/factories/infomations.rb

FactoryBot.define do
  factory :infomation do
    label            { :not }
    sequence(:title) { |n| "infomation(#{n})" }
    summary          { "#{title}の要約" }
    body             { "#{title}の本文" }
    started_at       { Time.current - 1.hour }
    ended_at         { Time.current + 3.hour }
    target           { :all }

※任意項目は頑張って設定すると、後で楽になる。

traitでベースのfactoryを上書きして共通化する

ステータス等の状態で、値が変わるケースが存在します。
全てをfactoryで定義すると冗長になるので、traitで共通化します。

spec/factories/downloads.rb

FactoryBot.define do
  factory :download do
    requested_at { Time.current - 2.hours }
    # status       { :waiting }
    model        { :member }
    target       { :all }
    format       { :csv }
    char_code    { :sjis }
    newline_code { :crlf }
    output_items { ['user.name'] }
    association :user

    # ステータス
    trait :waiting do
      # status { :waiting }
    end
    trait :processing do
      status { :processing }
    end

statusのデフォルトはMigrationで:waitingにしているので、不要なのですが、
分かりやすいようにコメントアウトで残しています。

この例では:waitingは:processingは、不要そうに見えますね。あまり変わらない。

- let_it_be(:download1) { FactoryBot.create(:download, :waiting) }
+ let_it_be(:download1) { FactoryBot.create(:download, status: :waiting) }
- let_it_be(:download2) { FactoryBot.create(:download, :processing) }
+ let_it_be(:download2) { FactoryBot.create(:download, status: :processing) }

では、これが追加されたら、どうでしょう?

    trait :success do
      status       { :success }
      completed_at { Time.current - 1.hour }
    end
    trait :failure do
      status        { :failure }
      error_message { 'エラー内容' }
      completed_at  { Time.current - 1.hour }
    end

traitで定義した方がシンプルだし、複数回使う場合は共通化できるようので良いですね。

- let_it_be(:download3) { FactoryBot.create(:download, :success) }
+ let_it_be(:download3) { FactoryBot.create(:download, status: :success, completed_at: Time.current - 1.hour) }
- let_it_be(:download4) { FactoryBot.create(:download, :failure) }
+ let_it_be(:download4) { FactoryBot.create(:download, status: :failure, error_message: 'エラー内容', completed_at: Time.current - 1.hour) }

ベースのfactoryのデフォルト値に期待しない
:waitingを外しても結果は同じなのですが、:waitingを期待するテストではあえて明示する事で、
可読性が上がるのと、ベースのfactoryの変更の影響を受けなくする事ができます。

- let_it_be(:download1) { FactoryBot.create(:download, :waiting) }
+ let_it_be(:download1) { FactoryBot.create(:download) }

※traitを上手く使うと共通化できる。その為には、ベースのfactoryが重要になる。

traitをfactory間で共有して共通化する

下記の:aaaは:userのスコープ内なので、他のfactory(≒モデル)では使えない。
(同名とかあり得るので、通常はこちらの方が都合が良い)
一方、:bbbは:user以外もスコープに入るので、他のfactoryでも使える。

spec/factories/users.rb

FactoryBot.define do
  factory :user do
    <省略>

    trait :aaa do
      <省略>
    end
  end

  trait :bbb do
    <省略>
  end
end

例えば、バリデーションをスキップしたい場合があります。
これは特定のfactoryだけでなく、他でも使う事があるので、外に置いて共通化したい。
どこかに入れても良いのですが解りに難くなるので、共通の入れ物を用意するのが良さそう。

spec/factories/application.rb

FactoryBot.define do
  trait :skip_validate do
    to_create { |instance| instance.save(validate: false) }
  end
end

※共通のtraitは外出ししておくと、依存がない事が明確になり解りやすい。

最後に(まとめ)

FactoryBotで頑張るか、Specで頑張るかの選択が必要なケースがあると思います。
FactoryBotで頑張ると可読性が悪くなる事が多いので、頑張らないで、
Specが長くなったとしても、そっちの方が解りやすい事が多い。
複数回使う可能性があるか?include_context→shared_contextで充分ではないか?

以前、書いたのもあるので、貼っておきます。

FactoryBot:リレーション先でリレーション元と同じidで作成されるようにする

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です