-
-
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!