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で充分ではないか?
以前、書いたのもあるので、貼っておきます。