Take, for example, a User model having a has_many relationship to an Address model. This is nothing out of the ordinary. Assume further that your business rules dictate that a User must have at least one address at all times and that there shall be no dangling addresses (without user reference) in the database. Also not uncommon. Presumably you would use nested attributes to create and update User and related Addresses in one go, but that’s not important here.
class User < ActiveRecord::Base
has_many :addresses, :inverse_of => :user
validates :addresses, :length => {:minimum => 1} # Rails 3 for validates_length_of
accepts_nested_attributes_for :addresses
end
class Address < ActiveRecord::Base
belongs_to :user, :inverse_of => :addresses
validates :user, :presence => true
end
Lastly, we want to use Factory Girl to create an object hierarchy consisting of a User and related Address, ideally in a single line atomic statement for use in tests across the entire application.
Factory.define :user do |f|
f.first_name "Joe"
f.last_name "Smith"
end
Factory.define :user_with_address, :parent => :user do |f|
f.addresses { |user| [user.association(:address)] }
end
Factory.define :address do |f|
f.association(:user) # This will lead to infinite recursion!
f.street "123 Main St"
f.city "San Francisco"
f.state "CA"
f.zip "94108"
end
The point of this exercise is to be able to create a User with associated address
Factory.create(:user)
or to create an Address with associated User, i.e.
Factory.create(:address)
Unfortunately, neither works when the validation rules are in effect. Why?
Factory.create to generate associated objects, even when I use Factory.build. So, Factory.build(:user) would try to invoke Factory.create(:address) under the hood, which fails, because the User association isn’t set up. I tried patching Factory Girl to have it call Factory.build under the hood. This gets past this issue, and Factory.create(:user) now works. However, Factory.create(:address) still fails, because::inverse_of feature does not set the live object on the belongs_to side of a has_many association. This has not (yet?) changed for Rails 3.0.3. There is actually a source code comment to this effect in code file belongs_to_association.rb:/activerecord-3.0.3/lib/active_record/associations/belongs_to_association.rb:
# NOTE - for now, we're only supporting inverse setting from belongs_to back onto
# has_one associations.
def we_can_set_the_inverse_on_this?(record)
@reflection.has_inverse? && @reflection.inverse_of.macro == :has_one
end
The effect of this issue is this:
irb> addr = Address.new
irb> user = addr.build_user
irb> user.addr
=> []
Given that, we cannot construct the object hierarchy by calling the factory for :address. We can create the hierarchy if we start with :user, but only if we patch Factory Girl’s proxy build. Neither is satisfactory.
There is another option to work around this problem by assigning the associations in an after_build hook. This is dissatisfactory as well, however, because it removes the ability to override the association attributes with parameters passed to the factory which is often necessary.
In conclusion, it seems that Factory Girl is not well equipped to construct object hierarchies that where the objects depend on each other for validation rules. Workarounds are possible, down to writing my own methods to construct the hierarchy piece by piece. That, however, is the kind of pedestrian code that the factory was supposed to address in the first place. I hate to write an article that doesn’t come out with a new brilliant and glorious conclusion. This is more of a call for new ideas.
Please comment if you know of other tools or workaround that deal with this situation better. I’ve been a long time fan of Factory Girl but I find these problems a bit too annoying to put up with. I’ll be taking a deeper look at Factory Girl-competitor Machinist next, although it is currently in an API-breaking transition from version 1 to version 2, and version 2 doesn’t implement associations yet. Perhaps it’s not the time to switch.
blog comments powered by Disqus