Object Mother Testing Pattern in Rails

Over the years I was working in Java at CGI, I became used to the Object Mother pattern of test setup. Despite the fact that it can break down when objects and their relationships get complicated, it is something that works quite well on smaller projects. At CGI we implemented it like this:

  • One factory per type of object being created
  • Each factory has a createNewValid method that creates a valid object that can be saved to the database. This method does not persist the object.
  • Each factory also has a createAnonymous method that calls createNewValid and then saves the object.

Rails has a number of plugins that do something similar, but object_daddy is the closest to what I was looking for. It follows the same pattern, calling the creation methods spawn and generate instead. It also allows you to customize the look of a valid object by letting you pass in a block that can modify the object’s state. This eliminates a huge problem with our implementation at CGI, where we had hundreds of slightly different createNewValid and createAnonymous methods depending on the scenario we were trying to set up.

Unfortunately I had a couple of problems with object_daddy. The first is that the generate method saves the object before it yields to a passed in block. Thus, when you call User.generate { |u| u.name = ‘Fred’ } you get an object with a name of Fred, but that name is not yet persisted. That was a minor issue, though.

I also had a problem when I tried creating multiple objects with object_daddy within the same setup. I would get an error stating that attributes had already been added for a class, and couldn’t figure out what the heck was going on. It could be just me (probably is), but I found the source code very hard to follow for something that should be pretty simple. Since I’m new to Ruby, I figured I’d just try doing it myself from scratch and maybe learn something in the process.

Enter object_mother. Right now it’s just about as basic as can be. I can think of numerous ways to improve it, but for now I’m pretty happy. Here’s the source in it’s entirety:

module ObjectMother
  class Factory
    @@index = 0
    def self.spawn
      raise 'No create method specified'
    end
    def self.populate(model)
      raise 'No populate method specified for ' + model.to_s
    end
    def self.to_hash(model)
      raise 'No to_hash method specified'
    end
    def self.unique(val)
      @@index += 1
      val.to_s + @@index.to_s
    end
    def self.unique_email(val)
      user, domain = val.split('@')
      unique(user) + '@' + domain
    end
    def self.create
      obj = spawn
      populate(obj)
      yield obj if block_given?
      obj
    end
    def self.create!(&block)
      obj = create(&block)
      obj.save!
      obj
    end
    def self.create_as_hash(&block)
      obj = create(&block)
      to_hash obj
    end
  end
end

All the major concepts are there; create equals createNewValid and create! is the same as createAnonymous. There’s also a create_as_hash method for converting a valid object to a params hash when you’re doing post :create type stuff in functional tests. Currently, you utilize the Factory by sub-classing in your rails app somewhere (I’m just using test_helper.rb for now). Here’s an example:

class AreaFactory < ObjectMother::Factory
  def self.spawn
    Area.new
  end
  def self.populate(model)
    model.name = unique('SW')
    model.city = CityFactory.create!
  end
end

The spawn method just creates a blank instance of the class each individual factory deals with and could definitely be inferred (baby steps, I’ll get to that once the annoyance factor gets higher). Overriding populate is where the attributes of a valid model object are set.

Overall, this is really just what I wanted, and the basic code is only a few simple lines of ruby. I’ll add some smarts in eventually (ie; inferring the class to spawn, creating hashes automatically, giving the user a default place to put Factories, making sure that passing blocks actually works), but for the time being I’m pretty happy.