# Rails developers have had bad experience with fixtures since a long time # due to several reasons, including misuse. # # This misuse of fixtures is often characterized by a huge ammount of # fixtures, causing a lot of data to maintain and dependence between tests. # In my experience working (and rescueing) different applications, 80% of # these fixtures are used only by 20% of tests. # # An example of such tests is a test that assures a given SQL query with # GROUP BY and ORDER BY conditions returns the correct result set. As expected, # we need a huge amount of data in this test which we usually don't need in # order tests. # # For such scenarios, factories are a fine solution. They won't clutter # all your database since they are created for these specific tests and they # are also easier to maintain. # # I believe this was the primary reason for the Rails community to strongly # adopt factories builders as we saw in the couple two years ago. # # However, factories are also misused. It is common to see people creating # a huge amount of data with factories before each test in their integration # suite, causing their whole test suite to be slow, while 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, use fixtures. For all the # other smaller scenarios, use factories. As both fixtures and factories # require valid attributes, this code below provides a quick solution # that allows you to create small, simple factories from the information # stored in your fixtures. # # == Examples # # You can define your builder inside the Builders module: # # module Hermes::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 Hermes::Builders # @@sequence = 0 # # def sequence # @@sequence += 1 # end # end # module Hermes module Builders ATTRIBUTES_REGEX = /valid_(.*?)_attributes$/ BUILDER_REGEX = /(create|new)_(.*?)(!)?$/ @@builders = ActiveSupport::OrderedHash.new class << self def build(name, options={}, &block) klass = options[:class] || name.to_s.classify.constantize builder = if options[:like] load_attributes_from_fixture(name.to_s.pluralize, options[:like], block) else block end @@builders[name] = [klass, builder] end def retrieve(scope, name, method, options) if builder = @@builders[name.to_sym] klass, block = builder hash = block ? block.bind(scope).call : {} hash.symbolize_keys! hash.merge!(options || {}) hash.delete(:id) [klass, hash] else raise NoMethodError, "No builder #{name.inspect} for `#{method}'" end end private def load_attributes_from_fixture(fixture_type, fixture_name, block) lambda { send(fixture_type, fixture_name).attributes.symbolize_keys.merge!(block ? block.call : {}) } end end def respond_to?(method) case method.to_s when BUILDER_REGEX true when ATTRIBUTES_REGEX true else super end end def method_missing(method, *args, &block) case method.to_s when BUILDER_REGEX 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 ATTRIBUTES_REGEX Builders.retrieve(self, $1, method, args.first)[1] else super end end end end