factory_girl and a tree of related objects
My fixture replacement of choice is factory_girl. In general, it’s been very good. But, as with any tool, there are edge cases that just don’t work as well. This is a common use case for me. It seems common enough that there should be a good solution for it, but I haven’t googled it up yet.
The problem
The pattern is that I have a root model, with a couple of collections living inside it.
class Site < ActiveRecord::Base
has_many :members
has_many :posts
end
One of the collections is made of objects that are just nested…
class Member < ActiveRecord::Base
belongs_to :site
has_many :posts
end
… and the other is associated with both.
class Post < ActiveRecord::Base
belongs_to :author, :class_name => 'Member'
belongs_to :site
end
(An obvious solution to this problem is to drop the
belongs_to :site
from Post, but that’s not always
desirable. Sometimes it just doesn’t work, if Post and
Member had a habtm association, and neither depended on
the other’s existence.)
The straightforward factories for the models above is this:
Factory.define :site do |x|
end
Factory.define :member do |x|
x.name 'example member'
x.association :site
end
Factory.define :post do |x|
x.title 'example post'
x.association :author, :factory => :member
x.association :site
end
The problem I run into is this: If I do Factory(:post)
, then
the post[id:1] has an author[id:1] linked to site[id:1]. But
post[id:1] is linked to site[id:2]!
Solution: Use an after_build callback
The best solution I’ve come up with is to define these associations
that depend on each other in an after_build
block. The simple way
to do it is just to try to hook up common associations.
Factory.define :site do |x|
end
Factory.define :member do |x|
x.name 'example member'
x.after_build do |member|
member.site ||= Factory.build(:site)
end
end
Factory.define :post do |x|
x.title 'example post'
x.after_build do |post|
post.site ||= post.author.try(site) || Factory.build(:site)
post.author ||= Factory.build(:member, :site => post.site)
end
end
Solution improved: after_build and a test-global context.
Another method that I like is to create a context class. Because most of my tests deal with data that is well-formed (e.g. it is unexpected that the application would contain a post that links to a member from a different site), the context class can store a global-to-test site instance.
class TestContext
class << self
def reset!
@instance = nil
end
def instance
@instance ||= new
end
def method_missing(*args)
instance.send(*args)
end
end
attr_accessor :site
def site!
self.site = Factory :site
end
end
Factory.define :site do |x|
end
Factory.define :member do |x|
x.name 'example member'
x.after_build do |member|
member.site ||= TestContext.site! || Factory.build(:site)
end
end
Factory.define :post do |x|
x.title 'example post'
x.after_build do |post|
post.author ||= Factory.build(:member)
post.site ||= post.author.site || TestContext.site! || Factory.build(:site)
end
end
class MyTestCase << Test::Unit::TestCase
def setup
TestContext.site!
end
def teardown
TestContext.reset!
end
def test_stuff
post = Factory :post
# ...
end
end
This works pretty well for me, but it seems like there should be a better way.
I think what I want is the concept of a fixed Site
instance for the duration
of a given test, so, a replacement for the TestContext.
The exact code in this article has not been tried, so it probably doesn’t run. Hopefully the intent is clear enough.