Is it possible to create an abstract factory? We want to use an abstract factory for the base class of an STI (Single Table Inheritance) model. But since the base class cannot be directly created in the database, currently our base factory cannot actually be instantiated. But we still want to use it as the base factory so that we can define the common elements (especially after build callbacks) there instead on the child factories. Is there a way to accomplish this in factory girl?
I guess the answer is no.
You could use a workaround like this:
FactoryGirl.define do
factory :base_class do
transient do
abstract_factory { raise "You can't instantiate this abstract factory." }
end
after(:build) do |runner, evaluator|
evaluator.abstract_factory
end
factory :child_class do
transient do
abstract_factory false
end
end
end
end
Alternatively you can prevent instantiating directly on the class itself, see http://stackoverflow.com/a/4834082/232838.
I think it would be a great feature, @axelson
@axelson In that situation, transient attributes are a great way to solve this.
One issue with the approaches outlined above is that the parent "abstract" factory will fail with an error when using the linter. I ended up using instance_eval to provide a sort of mixin for the common functionality, avoiding the definition of a factory for the abstract model altogether.
require 'faker'
FactoryBot.define do
define_shared_attributes = ->(*args) {
name { Faker::Name.name }
email { Faker::Internet.email }
password { Faker::Internet.password }
}
factory :company_user do
company
instance_eval &define_shared_attributes
end
factory :customer_user do
customer
instance_eval &define_shared_attributes
end
end
@tylerhunt another option for providing a base set of attributes - including callbacks and associations! - is traits.
@joshuaclayton I didn't quite understand how this would be done at first, but after playing with it a bit, I guess it would be something like this?
require 'faker'
FactoryBot.define do
trait :user do
name { Faker::Name.name }
email { Faker::Internet.email }
password { Faker::Internet.password }
end
factory :company_user, traits: %i(user) do
company
end
factory :customer_user, traits: %i(user) do
customer
end
end
This seems to work. The only real downside I see to this approach is the pollution of the global namespace with what's effectively a private trait.
Most helpful comment
I think it would be a great feature, @axelson