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