Skip to content

Instantly share code, notes, and snippets.

@billsaysthis
Forked from josevalim/builders.rb
Created October 1, 2010 15:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save billsaysthis/606387 to your computer and use it in GitHub Desktop.
Save billsaysthis/606387 to your computer and use it in GitHub Desktop.
# Rails developers have long had bad experiences with fixtures for
# several reasons, including misuse.
#
# Misuse of fixtures is characterized by having a huge number of them,
# requiring the developer to maintain a lot of data and creating dependencies
# between tests. In my experience working (and rescuing) many applications, 80%
# of fixtures are only used by 20% of tests.
#
# An example of such tests is one assuring that a given SQL query with
# GROUP BY and ORDER BY conditions returns the correct result set. As expected,
# a huge amount of data is needed for this test, most of which we won't be used
# in other tests.
#
# For these scenarios factories are a fine solution. They won't clutter up
# your database since they are created (and destroyed) during the execution
# of specific tests and are easier to maintain as the underlying models change.
#
# I believe this was the primary reason for the Rails community to strongly
# adopt factories builders over the few years.
#
# However, factories are also misused. Developers commonly create a huge
# amount of data with factories before each test in an integration
# suite, which causes their test suite to run slowly, where fixtures would
# work great for this purpose.
#
# This is a small attempt to have the best of both worlds.
#
# For the data used in almost all your tests, simply use fixtures. For all the
# other smaller scenarios, use factories. As both fixtures and factories
# require valid attributes, this quick solution allows you to create small,
# simple factories from the information stored in your fixtures.
#
# == Examples
#
# Define your builder inside the Builders module:
#
# module Builders
# build :message do
# { :title => "OMG", :queue => queues(:general) }
# end
# end
#
# The builder must return a hash. After defining this builder,
# create a new message by calling +create_message+ or +new_message+
# in your tests. Both methods accepts an optional options
# parameter that gets merged into the given hash.
#
# == Reusing fixtures
#
# The great benefit of builders is that you can reuse your fixtures
# attributes, avoiding duplication. An explicit way of doing it is:
#
# build :message do
# messages(:fixture_one).attributes.merge(
# :title => "Overwritten title"
# )
# end
#
# However, Builders provide an implicit way of doing the same:
#
# build :message, :like => :fixture_one do
# { :title => "Overwritten title" }
# end
#
# == Just Ruby
#
# Since all Builders are defined inside the Builders module, without
# a DSL on top of it, we can use Ruby to meet more complex needs,
# like supporting sequences.
#
# module Builders
# @@sequence = 0
#
# def sequence
# @@sequence += 1
# end
# end
#
## Source code
# Put it on test/supports/builders.rb and ensure it is required.
# May be released as gem soon.
module Builders
@@builders = ActiveSupport::OrderedHash.new
def self.build(name, options={}, &block)
klass = options[:as] || name.to_s.classify.constantize
builder = if options[:like]
lambda { send(name.to_s.pluralize, options[:like]).attributes.merge(block.call) }
else
block
end
@@builders[name] = [klass, builder]
end
def self.retrieve(scope, name, method, options)
if builder = @@builders[name.to_sym]
klass, block = builder
hash = block.bind(scope).call.merge(options || {})
hash.delete("id")
[klass, hash]
else
raise NoMethodError, "No builder #{name.inspect} for `#{method}'"
end
end
def method_missing(method, *args, &block)
case method.to_s
when /(create|new)_(.*?)(!)?$/
klass, hash = Builders.retrieve(self, $2, method, args.first)
object = klass.new
object.send("attributes=", hash, false)
object.send("save#{$3}") if $1 == "create"
object
when /valid_(.*?)_attributes$/
Builders.retrieve(self, $1, method, args.first)[1]
else
super
end
end
ActiveSupport::TestCase.send :include, self
end
## Some examples from a Real App™.
module Builders
build :profile, :like => :hugobarauna do
{ :username => "georgeguimaraes" }
end
build :user do
{
:email => "george@example.com",
:password => "123456",
:profile => new_profile
}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment