FactoryBotテクニック
FactoryBot.define do
factory :model_name do
field_name { 'value' }
factory :factory_name, class: ModelName do
field_name { 'value' }
end
# has_many関連
factory :factory_name2, class: ModelName do
transient do
count { 5 }
end
after(:build) do |model, evaluator|
model.has_many_models << build_list(:has_many_model, evaluator.count)
end
end
end
end
ケースごとにfactoryを分ける
以下のような1つのfactoryを色々なところで使おうとすると、ニーズが衝突して手に負えない状況に陥りやすい。
FactoryBot.define do
factory :user do
login_name { 'account' }
premium { true } # 有料会員かどうか
# ...
transient do
blogs_count { 5 }
end
after(:build) do |user, evaluator|
user.blogs << build_list(:blog, evaluator.blogs_count)
end
end
end
有料会員と無料会員を振り分けたい場合、テスト側でケアしなければならない。他のカラムやアソシエーションが影響を受けるなら、それらもテスト側で処理しなければならなくなる。
また、常にアソシエーション(上記の例ならuser.blogs
)も生成されることになるため、パフォーマンスが低下する。
そのため、以下のようにfactoryを分けた方が良い。
FactoryBot.define do
factory :user do
login_name { 'account' }
premium { false }
factory :normal_user, class: User do
factory :normal_user_has_blogs, class: User do
user_has_blogs
end
end
factory :premium_user, class: User do
premium { true }
factory :premium_user_has_blogs, class: User do
user_has_blogs
end
end
trait :user_has_blogs do
transient do
blogs_count { 5 }
end
after(:build) do |user, evaluator|
user.blogs << build_list(:blog, evaluator.blogs_count)
end
end
end
end
trait
はテスト側から参照しない
テスト側からtrait
を参照しない方が良い。参照したとしても、レゴブロックのようにtrait
を組み合わせるのではなく、すでにあるfactoryをチューニングするスイッチとして使った方が良い。
例えば、以下のようなfactoryを考える。
FactoryBot.define do
factory :user do
login_name { 'account' }
premium { false }
trait :normal_user
trait :premium_user do
premium { true }
end
trait :user_has_blogs do
transient do
blogs_count { 5 }
end
after(:build) do |user, evaluator|
user.blogs << build_list(:blog, evaluator.blogs_count)
end
end
end
end
このfactoryを利用するなら、create(:user, :premium_user, :user_has_blogs, login_name: 'suzuki')
のようになる。もし、user_has_blogs
がnormal_user
用のtraitでpremium_user
とuser_has_blogs
の組み合わせがデータ不整合を起こしていたとしても、テストが失敗しないかぎりは間違いに気づくことは難しいだろう。
このように、trait
をレゴブロックのように組み合わせる設計をしてしまうと、factoryを利用する側に組み合わせの整合性の責任を負わせることになる。そして、factoryはテストのあらゆる場所から利用されることを考えれば、まずい設計であることは明らかである。
したがって、整合性の責任はfactoryの定義内で負うべきである。factory :normal_user
やfactory :normal_user_has_blogs
のように整合性を持つ単位でfactoryを用意した方が良い。
trait
を定義するとしても、factoryから参照するだけでテスト側から参照しないようにするか、整合性に影響しないちょっとした設定を変更するスイッチとして利用した方が良い。
has_many
関連の定義
has_many
関連の定義は以下のようにする(trait
にするかは別として)。
FactoryBot.define do
factory :user do
trait :user_has_blogs do
transient do
blogs_count { 5 }
end
after(:build) do |user, evaluator|
user.blogs << build_list(:blog, evaluator.blogs_count)
end
end
end
end
user.blogs << build_list(:blog, evaluator.blogs_count)
ではなくcreate_list(:blog, evaluator.blogs_count, user: user)
としている例も見かける(というか、公式の例がそうなっている)。
しかし、create_list
としてしまうとbuild(:user, :user_has_blogs)
としたとしても、create(:user, :user_has_blogs)
としたのと同じ結果となりDBに保存されてしまう。上記の例のようにすれば、build
メソッドを使ったときにはインスタンスが生成されるだけでDBに保存されることはないし、create
メソッドを使ったときにはちゃんとDBに保存される。
テストのパフォーマンスに影響するので、上記のようにbuild_list
を利用した方が良い。
ディスクアクセスを最小化する
has_many
関連の定義もそうだが、ディスクアクセスが発生すれば、そのぶんパフォーマンスが低下することになる。したがって、ファイルからデータを読み込むようなfactoryは極力インスタンス化させないようにしなければならない。
FactoryBot.define do
factory :user do
trait :with_big_data do
big_data { File.read(Rails.root / 'spec/fixtures/files/user_big_data') }
end
end
end
上記のようにtrait
を使って、必要な場合にのみ参照させるか、factoryを分けてしまうなどの対応を取った方が良い。