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