# 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