In this blog post I will try to explain what my goals are for Letterpress.
As a consultant I have worked on many different Rails projects and most of them use model factories to populate their models with default values. I love to use model factories when writing tests. But I have one concern: I have to learn yet another DSL to be able to populate my models with default values. That feels wrong to me.
In The Ruby community we love to create DSLs, but DSLs always come with a cost. The cost of learning the DSL. And when creating a gem whose only purpose is to populate a model with default values. The DSL must be very small to add value.
In Letterpress you define an association with an array, see code below.
# ...
# make - returns the proxy object.
# new - returns the comment object populated with the default values.
comments { [ Comment.make.new, Comment.make.new ] }
# ...
When I write tests I sometimes want to do something with the object before using it; in Letterpress .make
returns a proxy object where behavior can be added without having to modify the object under test.
Since blueprints in Letterpress are constructed with Ruby classes you can add functionality to one of the blueprint classes or to all of them. If you add a method to the ProxyMethods module, all proxy objects will have that method. See the code example below.
# file: blueprint.rb
module Letterpress
module ProxyMethods
def save_and_reload
record = save
@object = @object.class.find(record.id)
end
end
class Post < Blueprint(ProxyMethods)
default do
username { "a username" }
end
def i_am_only_on_this_proxy_object
@object.tap { puts "I am only present on the Letterpress:Post instance" }
end
end
end
# make - returns the proxy instance of the class Letterpress::Post.
# save_and_reload - returns the saved and fetched from the database Post class instance.
Post.make.save_and_reload # => returns an instance of the Post class.
Post.make.i_am_only_on_this_proxy_object
# puts "I am only present on the Letterpress:Post instance"
# => returns an instance of Post class.
As the code above shows we are using inheritance - Post < Blueprint(ProxyMethods)
- to
add functionality. You can - if you don't want to use inheritance - also create a
module that you include into the blueprint classes.
When I use a model factory I want the library to ensure that all my created objects are valid. Letterpress adds three methods to the proxy object:
new
- returns the object under test or raises an exception if the object isn't valid.new!
- returns the object under test, even if not valid.save
- returns the persisted object under test or raises an exception if the object isn't valid.
I recommended you to always use the new
and save
methods. The new!
method can
be used when testing validations, but when testing validations you can use the proxy object directly, see the code example below.
it "must be present" do
Post.make(title: nil).should have(1).error_on(:title)
Post.make(title: " ").should have(1).error_on(:title)
end
To make sure no unnecessary models are persisted. In Letterpress you dont't have to save the associated records inside the blueprint file. See code example below.
module Letterpress
class Post < Blueprint(ProxyMethods)
default do
title { "My blog title" }
# When defining an association, you create an array.
# The 2 comments will only be persisted in the database when Post.make.save
# is called, and not when Post.make or Post.make.new is called.
comments { [ Comment.make.new, Comment.make.new ] }
end
end
class Comment < Blueprint(ProxyMethods)
default do
body { "This is an comment." }
# I use Post.make.new to not persists the post record in the database.
post { Post.make.new }
end
end
end
Here is a short example what a spec file using Letterpress will look like.
# Inside post_spec.rb files
describe Post do
# I save this record once at the beginning since I test for uniqueness below.
let!(:post) { Post.make.save }
let!(:other_post) { Post.make.new }
# Is not needed since save will raise an exception if not valid.
specify { post.should be_valid }
describe "#title" do
it "must be present" do
Post.make(title: nil).should have(1).error_on(:title)
Post.make(title: " ").should have(1).error_on(:title)
end
it "must be unique" do
Post.make(title: post.title).should have(1).error_on(:title)
end
end
end
I have used Machinist in a lot of projects
and I like it very much. Letterpress has the same class method name .make
as Machinist.
But Letterpress returns a proxy object were Machinist returns the instance object.
I hope you will take Letterpress for a test drive.