About

Workbench is tiny, lean, mean, intuitive factory system that exploits some lesser known features of ruby to deliver a robust, easy to understand solution without the tangled mess of meta-magic.

(disclaimer: some meta-magic involved, but it is minimal)

Highlights:

  • Does not use method_missing. Builder modules, in the end, contain no magic (meta-programming only used to generate modules).

  • No DSL. Builders are defined by declaring ruby methods (can your editor jump DSL declarations in a file?). Easily call other builder methods to achieve inheritance (see example below) or whatever you please.

  • Operate on actual instances of the objects. Wield ruby.

  • No dependencies (not even ActiveSupport).

  • Works with any Object that defines .new (with no params) and #save. Will use #valid? and #save! if the methods exist.

Requirements

Builder works with Ruby 1.8.7 and above.

Why another factory system?

There are a lot of factory frameworks, why one more? I felt I had an idea to maximize simplicity and minimize the experience of the factory system getting in the way by following a few conventions that should cover most use cases, while keeping it robust by getting out of the way.

Workbench is less than 70 lines (excluding documentation), and every line counts. Moderately experienced rubyists should have no trouble understanding the code, and there is not that much to parse.

Usage

Workbench is very simple to use, but does require a small amount of knowledge for othings to be immediately clear. Grok the following:

Add to your Gemspec:

gem "workbench"

Create a module in any place of your choosing:

spec/support/workbench_builders.rb

module WorkbenchBuilders
  extend Workbench

  ... your code here. See sample below ...
end

Now, include the module anywhere you want (yes, you can even include it in your ERB views, you sick freak). Most will probably want to include it in their global test config or RSpec config (like so):

RSpec.configure do |config|
  include WorkbenchBuilders
  ...
end

Sample Builders

module WorkbenchBuilders
  # Activate Workbench for this module. MUST go at the top.
  extend Workbench

  # Declare builder for model User (class name is infered by the
  # method name).
  #
  # A builder method may receive between 1 and 3 arguments. (due to
  # shortcoming in ruby 1.8.7, if you specify any optional
  # arguments, Workbench will assume it can send all 3 arguments).
  #
  # The following methods will be automatically added in this module:
  #
  # * new_user(attributes)
  # * create_user(attributes)
  # * find_or_create_user(attributes)
  #
  # user_defaults will be called with a new instance of User. Any
  # attributes provided to new_user et al will be sent to the User
  # instance via user#send("#{attribute_name}=", value)

  def user_defaults(u, n)
    u.name ||= "Name #{n}"
    u.code ||= "U#{n}"
    u.role ||= "user"
  end

  # An admin user. Since we intend to build a User and not an
  # AdminUser (the would-be inferred class name), we need to declare
  # the class name just before the builder method definition.
  #
  # The following methods will be automatically added in this module:
  #
  # * new_admin_user(attributes)
  # * create_admin_user(attributes)
  # * find_or_create_admin_user(attributes)
  #
  # Caveat: find_or_create_admin_user is not aware of the role, so
  # it will return a non-admin user just the same that matches the
  # attributes you provide.

  use_class :User
  def admin_user_defaults(u, n)
    u.role ||= "admin"
    user_defaults(u, n)
  end
end

See:

  • Workbench

  • Workbench#use_class

  • Workbench#count_with

Counters

Counters are scoped to the class name. In the following example, user and admin_user will count together:

module Builders
  extend Workbench
  def user_defaults(u, n)
    ...
  end

  use_class :User
  def admin_user_defaults(u, n)
    ...
  end
end

new_admin_user # n = 1
new_user       # n = 2
new_admin_user # n = 3

In cases where have two builders that use different classes but the same table and require them to count together, use #count_with:

module Builders
  extend Workbench

  count_with :Publication
  def book_defaults(u, n)
    ...
  end

  count_with :Publication
  def article_defaults(u, n)
    ...
  end
end

new_book        # n = 1
new_publication # n = 2
new_book        # n = 3

Resetting counters

If you do clear your database for every test, run the following before or after each test is run. (a la config.before(:each)…)

Workbench.reset_counters!

Otherwise you may have tests that fail due to an increment getting so big that it breaks a length validation, or something of the sort. A failure that would probably not fail if the test were run individually.

A note about false / nil

Workbench builders contain a lot of conditionals, testing for nil or false to see if a value is populated. This is problematic, naturally, if you need to provide nil or false as an override value.

IE:

def user_defaults(u)
  u.name ||= "Bob"
end

new_user(:name => nil)

Workbench is opinionated as follows:

  • Only set the bare minimum: If a model accepts false or nil as a valid value, you should consider leaving it false or nil by the builder.

  • When designing data models, Nil / False should represent the neutral, non-exceptional case: IE: prefer User#has_admin_priviledges over User#deny_admin_privileges.

  • You should never provide attributes to new_<model> or create_<model> that produce an invalid object. When testing validation, instantiate a valid instance with new_<model> and then modify it outside of the builder as follows:

    context "validation"
      it "requires a name field" do
        u = new_user
        u.name = nil
        u.should have(1).errors_on(:name)
      end
    end

These conventions should work for 99% of scenarios. In the case you find a good reason to override an attribute with nil or false, defined your builder method to receive 3 arguments to receive the overrides hash:

def user_defaults(u, n, overrides = {})
  u.name = "Bob" unless overrides.has_key?(:name)
end