Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save unders/2773668 to your computer and use it in GitHub Desktop.
Save unders/2773668 to your computer and use it in GitHub Desktop.
Yet another fixture replacement model factory gem - Introducing Letterpress

Title: Introduction Letterpress - the model factory for the lazy people.

Why yet another model factory gem?

In this blog post I will try to explain what my goals are for Letterpress.

No DSL

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 comes with a cost. The cost of learning the DSL. And when creating a gem who's 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 ] }
# ...
Extendable - Proxy Object

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.

Fail Fast

When I use a model factory I want the library to ensure that all my created objects are valid. Letterpress adds tree 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, event 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
Speed - Don't persist

To make sure no unnecessary models are persisted. In Letterpress yo 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

A Test example

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

Inspiration

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.

Documentation

Other model factories

I hope you will take Letterpress for a test drive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment