factory_bot icon indicating copy to clipboard operation
factory_bot copied to clipboard

Forwarding from one factory to another

Open amomchilov opened this issue 3 years ago • 3 comments

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:

  1. The fake image_url we generate is more realistic than an entirely random URL, because it's that's based off the title and subtitle
  2. We have an index that ensures FooRecord IDs are unique. We want this to be the case, regardless of whether you call create(:foo_record) or create(:foo). We want them to share the same ID so that the records can never collide, regardless of their origin.
  3. We have a lot more fields, which invoke Faker in various ways. We don't want to duplicate this logic between the two factories.
  4. 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?

amomchilov avatar May 25 '21 19:05 amomchilov

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.

composerinteralia avatar Jul 02 '21 16:07 composerinteralia

Oh that's an interesting approach. I'll try playing around with it

amomchilov avatar Jul 08 '21 14:07 amomchilov

Looks like there was also some related conversation in https://github.com/thoughtbot/factory_bot/issues/985

composerinteralia avatar Jul 15 '21 19:07 composerinteralia