ObjectMomma
An Object Mother implementation in ruby
Description
ObjectMomma is a gem that implements the Object Mother pattern for managing test data. It shares a lot of goals with FactoryGirl, but represents a fundamentally different approach. See this article by Martin Fowler for more information.
Vs. Fixtures and Factories
The Object Mother pattern shares some benefits with fixtures -- namely, by assigning fixed names (I call them child identifiers) to the objects you define, you can mentally associate the properties of each object with those names. For instance, the user called "Joe Spammer" may be used pervasively throughout tests that cover comment moderation. You can even build larger scenarios that can be re-used over and over by your team.
However, unlike fixtures, instead of managing yml files that directly manipulate database state, you can build out your objects in ruby with all of your model behaviors available. In this way, you get some of the best benefits of both fixtures and traditional factories.
The biggest benefit of Object Mother over both fixtures and factories is that they are not tied to ActiveRecord, so you can actually use ObjectMomma to refactor your entire model structure if you need to. ObjectMomma's goal is to facilitate data set up for your acceptance tests, not unit tests, so there is no reason that you couldn't use ObjectMomma alongside FactoryGirl or fixtures. This approach is similar to using cucumber for acceptance tests and rspec or Test::Unit for your unit/controller tests.
Installation
Install the gem locally:
gem install object_momma
Or add it to your Gemfile:
gem 'object_momma'
and run bundle install
from your shell.
Usage
How your spec should look
rspec/capybara
:
feature "Moderating comments" do
let(:post) do
ObjectMomma.post("Birds")
end
background do
ObjectMomma.spawn_comment("Joe Schmoe's Comment on the Post about Birds")
end
scenario "Up voting a comment" do
visit post_path(post)
# etc.
end
end
Test::Unit
:
class CommentModeration < ActionController::IntegrationTest
def setup
@post = ObjectMomma.post("Birds")
ObjectMomma.spawn_post("Joe Schmoe's Comment on the Post About Birds")
end
def test_upvoting_a_comment
visit post_path(post)
# etc.
end
end
Using ObjectMomma
To create new objects, use methods that start with ObjectMomma.spawn_*
, e.g. ObjectMomma.spawn_user
:
=> user = ObjectMomma.spawn_user("Joe Schmoe")
-> <#User:0xdeadbeef>
In this case above, "Joe Schmoe" can be thought of as the child identifier for the particular child we're creating.
Some objects are composed of other objects, and they can actually nest those child identifiers neatly, e.g. Joe Schmoe's Comment on the Post about Birds.
You can spawn them by either referring to their composite child identifier, or by a hash mapping the associated object names to their identifiers:
=> comment = ObjectMomma.spawn_comment("Joe Schmoe's Comment on the Post about Birds")
=> comment = ObjectMomma.spawn_comment(user: "Joe Schmoe", post: "Birds")
ObjectMomma.spawn_*
will raise an ObjectMomma::ObjectExists
exception if the object has already been spawned. To simply return the object if it does exist, omit the spawn_
and invoke ObjectMomma thusly:
=> ObjectMomma.spawn("Joe Schmoe") # Works the first time
=> ObjectMomma.spawn("Joe Schmoe") # Now raises an ObjectExists exception
=> ObjectMomma.user("Joe Schmoe") # Works fine :)
If you want to raise an error if the object doesn't exist, then ObjectMomma.find_*
will raise an ObjectMomma::ObjectNotFound
exception in that case:
=> ObjectMomma.find("Joe Schmoe") # Raises ObjectNotFound exception
=> ObjectMomma.spawn("Joe Schmoe") # Works fine :)
=> ObjectMomma.find("Joe Schmoe") # Does not raise exception
You can also build a bunch of objects all at once with ObjectMomma.spawn
:
=> ObjectMomma.spawn({
posts: ["Birds", "Middle Earth", "Sports"],
users: ["Joe Schmoe", "Scott Pilgrim"],
comment: "Billy Pilgrim's Comment on Post about Cooking"
})
The idea with the single call to ObjectMomma.spawn
is to be able to encapsulate complicated data setup with a simple, semantic call.
Teaching ObjectMomma how to build new types of objects
The examples above assumed that ObjectMomma knew how to build out the objects in question. To actually teach ObjectMomma how to build the objects, consider:
In spec/object_momma/user.rb
:
class ObjectMomma::UserBuilder < ObjectMomma::Builder
def first_or_initialize
User.where(full_name: self.child_id)
end
def build!(user)
user.full_name = child.child_id
end
end
The method #first_or_initialize
has the most important role of the builder: it grabs the object from the persistence layer if it exists, otherwise, it initializes a new object.
After grabbing an object from #first_or_initialize
, ObjectMomma will invoke #build!
if and only if the object hasn't yet been persisted. #build!
will actually set up the data and persist the object itself.
Composed Builders
You can also implement builders that know how to compose objects from their associated objects:
class ObjectMomma::CommentBuilder < ObjectMomma::Builder
child_id { "#{author}'s Comment on #{post}" }
has_siblings :post, author: user
def first_or_initialize
Comment.where(user_id: self..id, post_id: self.post.id)
end
def build!(object)
if object.user.spammer?
object.text = "Check out teh cheap pillz from mycheappillz.com!!!"
elsif object.post.subject == :birds
# etc. …
end
end
end
It is important to note the distinction between self
and object
in these cases. self
refers to the builder which has been hydrated with properties from the child_id
block, whereas object
refers to the child object itself.
But I don't want to use ActiveRecord!
The only requirement ObjectMomma imposes on the objects that are spawned via your first_or_initialize
methods is that they respond to #persisted?
the way ActiveRecord objects do. The gem itself does not depend on ActiveRecord at all; feel free to use whatever persistence layer you want. Consider overriding ObjectMomma::Builder.is_persisted?
if your objects don't respond to #persisted?
.
Your builder's first_or_initialize
method can return a Scope, as well. ObjectMomma will simply call first_or_initialize
for you.
Serialized Attributes
Supplying all the fictional data for your different children can be tedious and ugly. You may want to store canned attributes for your objects in YAML files, similar to fixtures. This features is completely optional. The attributes will get passed to your builders' #build! method as the second argument. An empty hash will be supplied if ObjectMomma couldn't find and parse the attributes for you. Example:
# spec/object_momma/user_builder.rb
class ObjectMomma::UserBuilder
The YAML file:
# spec/object_momma/attributes/users.yml
Scott Pilgrim:
username: "scott_pilgrim"
email: "[email protected]"
To use:
=> ObjectMother.spawn("Scott Pilgrim")
-> #<User:0xdeadbeef @username="scott_pilgrim", @email="[email protected]", …>
Business in the front, party in the back
If you'd like to refer to ObjectMomma via the constant ObjectMother, then have it your way:
In spec/spec_helper.rb
, add ObjectMomma.mullet!
somewhere. Then:
=> ObjectMother.spawn_user("Joe Dirt")
More Information
Credits
ObjectMomma was written by Nathan Ladd, with help from a few partners in crime:
- Josh Flanagan (jflanagan on github)
- Theo Mills
And, of course, I read about the ObjectMother pattern that I ~~ruined~~ implemented from:
- Martin Fowler