-
-
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 => "[email protected]", | |
:password => "123456", | |
:profile => new_profile | |
} | |
end | |
end | |
test "users sets profile gravatar on save" do | |
user = create_user! | |
assert_equal Digest::MD5.hexdigest("[email protected]"), user.profile.gravatar | |
end |
Done!
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.
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.
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.
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!