Skip to content

Instantly share code, notes, and snippets.

@josevalim
Created October 1, 2010 12:25
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save josevalim/606129 to your computer and use it in GitHub Desktop.
Save josevalim/606129 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
test "users sets profile gravatar on save" do
user = create_user!
assert_equal Digest::MD5.hexdigest("george@example.com"), user.profile.gravatar
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