Skip to content

Instantly share code, notes, and snippets.

@josevalim
Created October 1, 2010 12:25
Show Gist options
  • Select an option

  • Save josevalim/606129 to your computer and use it in GitHub Desktop.

Select an option

Save josevalim/606129 to your computer and use it in GitHub Desktop.
# A couple years ago the Rails community discovered the joys of factory
# builders as an alternative to fixtures. Rails developers have had
# bad experience with fixtures since a long time due to several reasons
# including misuse.
#
# This misuse is often characterized by a huge ammount of factories,
# causing a lot of data to maintain and dependence between tests. In my
# experience working (and rescueing) different applications, 80% of these
# factories are used only by 20% of tests (which are usually tests that
# need a huge ammount of data to test different scenarios).
#
# Factory builders provides an alternative for you to easily build different
# scenarios based on a commont set of factories. However, if you have
# integration tests, creating the basic structure before each tests using
# factories is definitely expensive.
#
# This is an attempt to have the best of both worlds by incentivating
# you to use fixtures but providing helpers similar to factories built
# on top of your fixtures.
#
# == Examples
#
# You can define your builder inside the Builders module:
#
# module Builders
# build :message do
# { :title => "OMG", :queue => queues(:general) }
# end
# end
#
# It should necessarily return a hash. After defining this builder,
# you can easily create a new message calling +create_message+ or
# +new_message+ in your tests. Both methods accepts an optional
# options parameter that is 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, it allows us to use Ruby in case we need to do
# something more complex, 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
@mikegehard
Copy link

Looks like an interesting idea....

Can you post some example code on what a test might look like that used one of these builders? It might help me wrap my head around how I would use them.

Thanks!

@josevalim
Copy link
Author

Done!

@mikegehard
Copy link

A very interesting approach. What are your thoughts on handling updating the fixtures when you make a model change, say add a new validation? For a factory approach you would update the factory file but for fixtures you may still need to update a bunch of fixtures.

@josevalim
Copy link
Author

The idea is to have few fixtures, reducing considerably the impact and pain caused by such changes. This is the second project I am using this approach and it is working fine. For instance, I usually have two/three users which I use in my integration tests and few data. I don't let it grow much beyond it.

Another thing that helps is a tip from 37 Signals (which I believe it was in Getting Real book): always use real names and data in fixtures, try to create a story. This helps you to stay concise and don't lose track of your data.

@trevorturk
Copy link

I like it! It looks to me like you could also set attributes like ":email => Faker::Internet.email" if you want to use this like factories more or less, but I'm not sure...?

The reason I like to use machinist myself isn't quite for the reason you said. It's because I want my tests to be isolated most of the time. So, I want to have tests where I can easily see exactly what's all there, and I don't have to worry about there being other users or records around that might screw things up.

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