thoughtbot / factory_bot

A library for setting up Ruby objects as test data.
https://thoughtbot.com
MIT License
7.92k stars 2.61k forks source link

Attributes for factories associations #693

Closed douglascamata closed 10 years ago

douglascamata commented 10 years ago

I got this example factory:

FactoryGirl.define do 
  factory :foo do
    bar
  end

  factory :bar do
  end
end

And in my specs I needed to set specific attributes for the bar object inside each foo I was creating and it's really boring. The solution was something like:

bar1 = create :bar, attribute: value
bar2 = create :bar, attribute: value

foo1 = create :foo, bar: bar1
foo2 = create :foo, bar: bar2

Maybe you (or we) could improve this to something like:

foo1 = create :foo, bar: { attribute: value1 }
foo2 = create :foo, bar: { attribute: value2 }

Of course, nesting more levels should be possible.

passalini commented 10 years ago

+1

sales commented 10 years ago

+1

RafaelMCarvalho commented 10 years ago

+1

vkmagalhaes commented 10 years ago

+1

joshuaclayton commented 10 years ago

@douglascamata given how FactoryGirl works, there's no way to tell that bar is an ActiveRecord association; all it's doing is raw assignment by calling foo#bar=, passing in the built object.

That said, you should be able to use accepts_nested_attributes_for :bar on the model, and pass bar_attributes: { attribute: value1 } and have everything work.

douglascamata commented 10 years ago

@joshuaclayton how come this snippet works if FactoryGirl can't tell if bar is another factory? I use this syntax in all my factories and FactoryGirl knows these kind of attributes are other factories and creates them for me. And I can tell you right away they are associations factories, 'cause the method calls doesn't have any arguments.

And, as seen in the docs, there's another syntax for associations that is pretty meaningful and can be used to solve issues like this:

factory :post do
  # ...
  association :author, factory: :user, last_name: "Writely"
end 

Totally explicit that author is an association and would be possible to use that syntax I proposed to override its attributes.

The idea of modify my app's code just to be able to make my tests prettier and smaller is really annoying, IMHO.

joshuaclayton commented 10 years ago

@douglascamata from a declaration perspective, yes, declaring the association with arbitrary attributes works correctly. When actually building the object, though (e.g. create :foo, bar: { attr1: 'value' }), it's not aware of the fact that it's an association.

Some of the code related to this can be viewed here and here.

At the stage of providing overrides, the only thing the evaluator has is the method signatures returning processed values - so for an override, https://github.com/thoughtbot/factory_girl/blob/7e2bed5938be2e1314a7df6a647031a47d01c2c8/lib/factory_girl/evaluator.rb#L71, or for an association (that's not been overridden, provided completely by the factory definition), https://github.com/thoughtbot/factory_girl/blob/7e2bed5938be2e1314a7df6a647031a47d01c2c8/lib/factory_girl/attribute/association.rb#L19.

I think overloading associations, either by accepting relational objects or hashes of attributes, muddles the interface and may cause confusion. Extending this so hashes can be nested further introduces indirection here, in my opinion. I appreciate the brevity of assigning sets of attributes by passing around hashes, but again, there are mechanisms for this already built into ActiveRecord (proposed above).

Is there another mechanism that could be used here, independent of Factory Girl, which strikes a better balance? Can pure Ruby be used instead?

boardfish commented 3 years ago

This still holds up, and you could take it further by having a transient attribute that's a hash and spreading it:

    factory :parent do
      transient do
        child_attributes { { sensible_default: 'can be specified here' } }
      end

      association :child, **child_attributes
    end

However, it doesn't give you the usual FactoryBot freedom of changing one attribute (from the default) at a time. So I think there's still a need for this if it doesn't exist already.

jdufresne commented 3 years ago

FWIW, this feature is built into factory_boy which was originally inspired by factory_bot. Coming from Python, I miss this feature. That project uses a double underscore syntax to traverse factory relationships, but the general concept is there.

Here is an example from the docs:

https://factoryboy.readthedocs.io/en/stable/reference.html#calling

Fields of the SubFactory may be overridden from the external factory:

>>> c = CompanyFactory(owner__first_name='Henry')
c.owner
<User: Henry Doe>

This would be handy for dependent attributes in the association's factory.