Rolling your own object creation methods for specs

October 23, 2011 Pivotal Labs

Lately my favorite way to create objects in my spec suite is to use an object mother pattern. There are a number
of object mother libraries to choose from (see ruby toolbox for a few), but it’s such an easy pattern
to implement that lately I’ve just been rolling my own. In this post I’ll describe what I’ve been using recently and
why I’ve chosen it over using gems.

To implement a basic object mother pattern, all you need to do is define a few methods that are available to your specs, like so:

def new_post(overrides = {})
  Post.new( {:title => "Some title"}.merge(overrides) )
end

def create_post(overrides = {})
  new_post(overrides).tap(&:save!)
end

This allows you to initialize a new, valid Post, or create one with a single method call:

new_post
create_post
create_post(:title => "Some other title")

You might notice that if you use attr_protected for the Post#title attribute, the previous example wouldn’t work.
So the next step is to make sure that protected attributes are assigned correctly:

def new_post(overrides = {})
  Post.new do |post|
    post.title = "Some title"
    overrides.each do |method, value|
      object.send("#{method}=", value)
    end
  end
end

Let’s say you add a unique constraint the Post#title field. You’ll need to be able to generate a unique post name.
Here’s a quick and dirty (and probably non-thread-safe) way to create unique post titles:

def new_post(overrides = {})
  Post.new do |post|
    post.title = "Some title #{counter}"
    overrides.each do |method, value|
      object.send("#{method}=", value)
    end
  end
end

def counter
  @counter ||= 0
  @counter += 1
end

Associations are typically simple to deal with:

def new_comment(overrides = {})
  Comment.new do |post|
    comment.post = new_post
    overrides.each do |method, value|
      object.send("#{method}=", value)
    end
  end
end

This allows you to do the following:

comment = new_comment
comment.save! # => ActiveRecord will automatically save the post for you

comment = new_comment(:post => Post.first)
comment.save! # => will use the post you passed in

Notice that the new_comment method initializes a new post even if you pass in a post.
This might seem like a detail, but initializing the Comment object unnecessarily will increase the time it takes your
spec suite to run. This might be trivial, but in a large test suite it can add up. To see how much of an impact it
might have in your app, you can run some simple benchmarks:

Benchmark.realtime do
  1000.times { Comment.new :post => Post.new }
end

Benchmark.realtime do
  post = Post.new
  1000.times { Comment.new :post => post }
end

In an app that I’m working on now, it showed that initializing a new post took over twice as long as not doing it. With that in
mind, it’s easy to solve that problem:

def new_comment(overrides = {})
  overrides[:post] = proc { new_post } unless overrides.has_key?(:post)
  Comment.new do |comment|
    overrides.each do |method, value_or_proc|
      comment.send("#{method}=", value_or_proc.is_a?(Proc) ? value_or_proc.call : value_or_proc)
    end
  end
end

This way, the Post is only initialized when one isn’t passed in. With those pieces in place, an example file might look like this:

# spec/spec_helper.rb
RSpec.configure do |config|
  config.include ObjectCreationMethods
end

# spec/support/object_creation_methods.rb
module ObjectCreationMethods
  def new_post(overrides = {})
    defaults = {:title => "Some title #{counter}"}
    Post.new { |post| apply(post, defaults, overrides) }
  end

  def create_post(overrides = {})
    new_post(overrides).tap(&:save!)
  end

  def new_comment(overrides = {})
    defaults = {:post => proc { new_post }, :text => "some text"}
    Comment.new { |comment| apply(comment, defaults, overrides) }
  end

  def create_comment(overrides = {})
    new_comment(overrides).tap(&:save!)
  end

  private

  def counter
    @counter ||= 0
    @counter += 1
  end

  def apply(object, defaults, overrides)
    options = defaults.merge(overrides)
    options.each do |method, value_or_proc|
      object.send("#{method}=", value_or_proc.is_a?(Proc) ? value_or_proc.call : value_or_proc)
    end
  end
end

You might be wondering why you might roll your own rather than using an existing library
like Factory Girl or Fixjour. A few of the benefits are:

  • The methods are not generated by meta-programming, so IDEs like RubyMine (or editors that make use of CTags) can offer code completion and refactoring support
  • It’s plain ruby, and is unlikely to break as ActiveRecord updates itself, whereas using a 3rd-party library you can’t upgrade Rails until that 3rd-party library supports the new Rails version
  • When you have complex object graphs it’s easy to initialize or create objects in the exact manner you’d like, as opposed to potentially being constrained by the library you are using
  • Developers on the project don’t have to learn a 3rd-party library’s api or idiosyncrasies
  • It takes about the same time to write these methods as it does to define similar methods in libraries like FactoryGirl
  • There are only 2 plumbing methods to support the framework – it’s super simple to understand

One feature I’ve seen implemented in object mother libraries is support for attribute hashes, similar to:

def valid_comment_attributes(overrides = {})
  {:post => new_post}.merge(overrides)
end

Presumably these attribute hashes would be used for passing into controller specs. In practice, I’ve never seen this work
as expected. In the example above, rspec would happily pass a new Post object into the controller spec, but that could never happen
in real life. However, if you wanted those valid attributes, you could easily incorporate those into your home-rolled ObjectCreationMethods.

I’ve used this pattern on several recent projects ranging from Rails 2.2.2 on Ruby 1.8.6 to Rails 3.1 on Ruby 1.9.2 and
it just works.

About the Author

Biography

More Content by Pivotal Labs
Previous
Making math make sense to programmers
Making math make sense to programmers

Whether you're learning math for pleasure or profit (jumping on the Big Data bandwagon), there are times wh...

Next
Serving up different sized dynamic images based on device resolutions
Serving up different sized dynamic images based on device resolutions

Serving up different sized dynamic images based on device resolutions. Jason and I were working on an app ...

Enter curious. Exit smarter.

Learn More