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.
I always like the idea of a gem, to (a) make it easier to switch from one project to another, bring new people up to speed, etc, (b) encapsulate the knowledge about things like the speedup and the attr_protected thing mentioned above. I believe the slogan is “prefer mechanism to idiom” (I heard this from William Pietri, don’t know if it has been published anywhere).
On the other hand, I also see the point about how these gems seem a bit overengineered for what is a fairly simple task.
October 24, 2011 at 6:20 am
def new_comment(overrides = {})
Comment.new do |post|
comment.post = proc { new_post }
overrides.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
Few issues with this. You’re assigning #post to a proc and if there’s any type coercion etc. that’s not going to work, and if they don’t override then it’ll remain a proc. You’re also leaking state. Seeing as this isn’t in the final version, that it’s broken, comment is named post.. I assume it was just for the post anyway. :
Now you’re accepting procs for values. Okay. But your stated goal was to not call default init if they do pass in a value.
comment.post = overrides.fetch(:post) { new_post }
I’m surprised there isn’t a destructive fetch, that’s what you’d really want. Except for maybe this..
overrides[:post] = overrides.fetch(:post) { new_post }
Comment.new(defaults.merge(overrides), :without_protection => true)
October 24, 2011 at 5:27 pm
The early worm gets the bird,
This blog post is a Josh Susser, Ruby Rogues pick.
October 29, 2011 at 1:56 am