factory_bot
factory_bot copied to clipboard
Forwarding from one factory to another
Hey there, I'm having difficulty making one factory that forwards to another. I don't have a particular solution approach in mind, so I didn't use the "feature request" template. For all I know, this is already possible, and I just don't know how.
Background:
I have two related types, Foo
, and FooRecord
. FooRecord
is part of our persistence-layer, similar to an active record model. Foo
is a wrapper that encapsulates a FooRecord
. The goal is for Foo
to make business logic be database-agnostic, so that we can swap out the underlying record implementation without external disruption.
We have a factory for Foo
, which is used in most places, especially in our business layer.
We also have a factory for FooRecord
, which is used in tests of our persistence-layer.
Our attempt
Here's what our FooRecord
factory looks like factories look roughly like this:
FactoryBot.define do
factory :foo_record, class: FooRecord do
initialize_with { new(**attributes) }
transient do
_title { Faker::Commerce.product_name }
_subtitle { Faker::Commerce.color }
end
sequence(:id)
title { _title }
subtitle { _subtitle }
image_url { Faker::Internet.url(path: "/#{_product_title.parameterize}/#{_variant_title.parameterize}/image.jpg") }
# 10+ more fields are set here ...
trait :not_yet_loaded do
title { nil }
subtitle { nil }
image_url { nil }
# ...
end
end
end
As you can see, there are some parts of the FooRecord
factory that are useful for the Foo
factory to use:
- The fake
image_url
we generate is more realistic than an entirely random URL, because it's that's based off thetitle
andsubtitle
- We have an index that ensures
FooRecord
IDs are unique. We want this to be the case, regardless of whether you callcreate(:foo_record)
orcreate(:foo)
. We want them to share the same ID so that the records can never collide, regardless of their origin. - We have a lot more fields, which invoke
Faker
in various ways. We don't want to duplicate this logic between the two factories. - We have traits such as
#not_yet_loaded
, which we'd like to be able to use on both factories.
We tried to define a factory for Foo
which forwards onto FooRecord
, to piggy off all this existing functionality that it has:
FactoryBot.define do
factory :foo, class: Foo do
initialize_with do
new(foo_record: build(:foo_record, **attributes))
end
end
end
But as you can guess, this definition doesn't allow us to forward along traits.
Is there a way to achieve my goals in a nice clean way?
I've seen some people use an abstract (i.e. not meant to be instantiated) parent factory to solve this. Something like:
factory :foo_base, class: Object do
# defines all the common stuff
end
factory :foo_record, class: FooRecord, parent: :foo_base do
# anything specific to foo_record
end
factory :foo, class: Foo, parent: :foo_base do
# anything specific to foo
end
There is some relevant discussion about this in https://github.com/thoughtbot/factory_bot/issues/1409
We may explore adding something like factory :foo_base, abstract: true
to make this feature official, but I'm trying to gauge how common a use case it is. We did a little spike in https://github.com/thoughtbot/factory_bot/compare/abstract-parent-factories?expand=1 We'd probably also want to raise if somebody tried to actually build an abstract factory.
Oh that's an interesting approach. I'll try playing around with it
Looks like there was also some related conversation in https://github.com/thoughtbot/factory_bot/issues/985