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.