Factory_bot: Abstract factory

Created on 2 Feb 2017  路  6Comments  路  Source: thoughtbot/factory_bot

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?

Most helpful comment

I think it would be a great feature, @axelson

All 6 comments

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.

Was this page helpful?
0 / 5 - 0 ratings