背景
- Factorybotを利用してテストデータを作成しているプロジェクトに
traitとtransientを使い分けているファイルを発見した。 -
traitは知っていたがtransient,evaluatorを知らなかったので残しておく
結論
-
traitは、状態の切り替え -
transientは、テスト用の一時変数 -
evaluatorは、transientの値を取得するためのオブジェクト
traitとは?
同じfactoryに対する状態の違いを名前付きでまとめたものです。
例えば、下記のfactoryが定義されているとします。
# spec/factories/users.rb
FactoryBot.define do
factory :user do
sequence(:name) { |n| "user#{n}" }
sequence(:email) { |n| "user#{n}@example.com" }
admin { false } # ここで管理者をbooleanで判定する
end
end
このUserモデルのテストデータでは、adminというbooleanのカラムを保持しているとします。
ここがtrueであれば管理者であり、falseであれば管理者ではありません。
しかし、この記述のままだとcreate(:user)と呼び出すとadminフラグはfalseとして生成されることになります。
逆にadmin { true }と記述すると、管理者データであるユーザーデータのみを生成することになります。
そこでtraitを利用します。
# spec/factories/users.rb
FactoryBot.define do
factory :user do
sequence(:name) { |n| "user#{n}" }
sequence(:email) { |n| "user#{n}@example.com" }
admin { false }
# こんな感じでtrueを定義
trait :admin do
admin { true }
end
end
end
そしてcreate(:user, :admin)と呼び出してあげると、管理者であるユーザーデータを生成することができ、create(:user)と呼び出してあげると、管理者ではないユーザーデータを生成することができます。
これがtraitを利用して状態の違いを名前付きで管理するということです。
transientとは?
factoryを生成するときに使う一時的な変数です。DBには保存されず、factory内部のロジックで使うためだけに存在します。
例えば、ユーザーに紐づく投稿(Post)を任意の件数作成したい場合を考えます。
# spec/factories/users.rb
FactoryBot.define do
factory :user do
sequence(:name) { |n| "user#{n}" }
sequence(:email) { |n| "user#{n}@example.com" }
transient do
posts_count { 0 } # デフォルトは0件
end
after(:create) do |user, evaluator| # evaluatorは下記で説明しています
create_list(:post, evaluator.posts_count, user: user)
end
end
end
このように定義すると、以下のように呼び出せます。
# 投稿なしのユーザー
create(:user)
# 投稿5件を持つユーザー
create(:user, posts_count: 5)
# 投稿10件を持つユーザー
create(:user, posts_count: 10)
posts_countはUserモデルのカラムではなく、factory内でのみ使われる一時変数です。
evaluatorとは?
after(:create)などのコールバック内で、transientで定義した値にアクセスするためのオブジェクトです。
after(:create) do |user, evaluator|
# evaluator.posts_count で transient の値を取得できる
create_list(:post, evaluator.posts_count, user: user)
end
呼び出し時に渡したposts_count: 10という値は、evaluator.posts_countで取得できます。
create(:user, posts_count: 10)
# → evaluator.posts_count は 10 になる
transientとtraitの組み合わせ
transientとtraitは組み合わせて使うこともできます。
# spec/factories/users.rb
FactoryBot.define do
factory :user do
sequence(:name) { |n| "user#{n}" }
sequence(:email) { |n| "user#{n}@example.com" }
admin { false }
transient do
posts_count { 0 }
end
trait :admin do
admin { true }
end
trait :with_posts do
transient do
posts_count { 3 } # traitでデフォルト値を上書き
end
after(:create) do |user, evaluator|
create_list(:post, evaluator.posts_count, user: user)
end
end
end
end
# 投稿3件を持つ管理者ユーザー
create(:user, :admin, :with_posts)
# 投稿10件を持つ管理者ユーザー
create(:user, :admin, :with_posts, posts_count: 10)
感想
- specの見通しを考えるとあまり乱用は避けた方がいいのかな?と思いつつ、かなり便利に作れそうだなぁと思った。
参考
公式はGETTING_STARTED.mdよりも↓を読むことを推奨しているらしい…