thoughtbot / factory_bot

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

Access to constructor attributes hash within dynamic attribute block? + nested association #583

Closed gamov closed 10 years ago

gamov commented 10 years ago

I tried asking this twice in the Google Group but both time my post disappeared…:

I would like to do different things if a certain attribute is passed to the constructor or not (using default), e.g.:

quantity { constructor[:quantity] ? constructor[:quantity] + 5 : 10 }  #lazy attribute

so the quantity will have different value if I specify the quantity or not in the constructor:

FactoryGirl.build :item, quantity: 4   #==> item.quantity == 9
FactoryGirl.build :item   #==> item.quantity == 10

Is this possible?

PS: In the getting started document, I think it would be good to specify that association attributes are processed before other attributes even though they could be defined after them in the define block.

joshuaclayton commented 10 years ago

What you're looking for are transient attributes.

Best of luck!

gamov commented 10 years ago

I'm aware of the transient attributes, however, in my case, I don't want the user of the factory to have to remember to use the transient attribute. My real case is of course more complicated than this one. Basically, I have a quantity dependency that I need to handle if user set a quantity attribute (by setting the association quantity > quantity).

No 'evaluator' I could tap in the block?

joshuaclayton commented 10 years ago

@gamov can you provide the real example? I'm not sure changing quantity like that is actually ideal because it introduces a level of indirection (if you set a value, I'd assume that value be the value assigned and not change).

gamov commented 10 years ago

First of all, thanks for your help. I switched around the requirement and it leads to cleaner factories and only broke a few tests which were easily fixed. Now quantity of the nested association is always valid.

class ShippingItem < ActiveRecord::Base
   belongs_to :purchased_item
   validate { errors.add(:quantity, "cannot be higher than available quantity: #{purchased_item.quantity}") if purchased_item.quantity < quantity }
end

Shipping Item factory:

factory :shipping_item do
    quantity { rand 3..100 }
    purchased_item {association :purchased_item, quantity: (quantity..(quantity + 100)).to_a.sample}
end

This works fine if you set the quantity or not when invoking the shipping item factory (which was my original question). Let say now that I want to set the delivery site for the purchased item from the shipping item factory. I would do:

factory :shipping_item do
    quantity { rand 3..100 }
    ignore {delivery_site nil}
    purchased_item {association :purchased_item, 
                                quantity: (quantity..(quantity + 100)).to_a.sample,
                                delivery_site: (delivery_site || FactoryGirl.build :delivery_site)
                            }
end

However, I can see it becoming a pattern and doesn't feel 'right' or DRY. Is it the usual way?

joshuaclayton commented 10 years ago

@gamov okay, this is starting to make more sense now, and yes, I agree this isn't DRY.

In cases like this (especially because this is likely a simple case and there are MUCH more complicated ones out there), I actually create support classes for building complicated data (either because there's a LOT of associated data or certain aspects change and it impacts creation of other factories, like this). The example I point to in cases like this is here: https://github.com/thoughtbot/factory_girl/issues/230#issuecomment-22414812

At the end of the day, factory_girl can't (and won't!) handle every permutation of every way objects get associated and built. It's just not possible. In cases like yours where there's little things that change, I always recommend solving things with pure Ruby. It's flexible, extensible, and technically testable if you want.

Hopefully this helps - I know "factory_girl can't handle this well" doesn't sound ideal, but it really is, since it keeps factory_girl's interface clean and understandable and removes the chance for odd edge cases.

gamov commented 9 years ago

I still wish we could access the constructor supplied hash to check what has been supplied to make some choice in the lazy attribute block…

http://stackoverflow.com/questions/31205859/factory-girl-can-we-check-if-an-attribute-has-been-overridden-in-its-lazy-attri