From cf1b8149e20ebf57d944e404d118fea1f1289ac3 Mon Sep 17 00:00:00 2001 From: Ginty Date: Sun, 22 Aug 2010 15:44:44 -0500 Subject: [PATCH] Significant refactoring to improve maintainability --- README.rdoc | 23 +++----- cranky.gemspec | 5 +- init.rb | 2 +- lib/cranky.rb | 118 ++++++------------------------------------ lib/cranky/factory.rb | 98 +++++++++++++++++++++++++++++++++++ lib/cranky/job.rb | 44 ++++++++++++++++ lib/cranky/version.rb | 3 ++ rakefile | 4 +- spec/cranky_spec.rb | 50 ++++++++---------- spec/spec_helper.rb | 34 ++++++++---- 10 files changed, 222 insertions(+), 159 deletions(-) create mode 100644 lib/cranky/factory.rb create mode 100644 lib/cranky/job.rb create mode 100644 lib/cranky/version.rb diff --git a/README.rdoc b/README.rdoc index 9b00119..053b56a 100644 --- a/README.rdoc +++ b/README.rdoc @@ -29,7 +29,7 @@ You can create as many different factory files as you want, just require them in == In a Nutshell -Cranky steals its core syntax from Factory Girl... +Cranky steals its core syntax from Factory Girl and can drop into tests already written for that framework... Factory.build(:user) # Build a user instance without saving Factory.create(:user) # Build and save a user instance @@ -50,7 +50,7 @@ Cranky has a nice debug option (rails only) to warn you when your factory is bro Cranky allows you to build factories via std Ruby methods, like this... # factories/my_factories.rb - class Cranky # Your factory must use the Cranky class + class Cranky::Factory # Your factory must extend Cranky::Factory # Simple factory method to create a user instance, you would call this via Factory.build(:user) def user @@ -89,14 +89,14 @@ This is where Cranky really shines, if you can create Ruby methods you can prett The only rules are: -1. Your factory must use the +Cranky+ class +1. Your factory must extend the +Cranky::Factory+ class 2. Your factory method must return the object you wanted to create 3. You can access the overrides passed in via options[:key] (not really a rule!) So for example to create a simple user factory... # factories/my_factories.rb - class Cranky + class Cranky::Factory # Simple factory method to create a user instance, you would call this via Factory.build(:user) def user @@ -114,7 +114,7 @@ Now of course you are working in straight Ruby here, so you can extend this any For example here it is with the capability to automatically create a default address association... # factories/my_factories.rb - class Cranky + class Cranky::Factory # Return the default address if it already exists, or call the address factory to make one def default_address @@ -235,17 +235,8 @@ Clear all instance variables in the factory. This may be useful to run between t Sometimes it is useful to be warned that your factory is generating invalid instances (although quite often your tests may intentionally generate invalid instances, so use this with care). By turning on debug the Factory will raise an error if the generated instance is invalid... - Factory.debug = true - -Or run within a block... - - Factory.debug do - Factory.build(:user) - end - -Or inline (runs the build method with debug enabled)... - - Factory.debug(:user) + Factory.debug(:user) + Factory.debug!(:user) Note that this relies on the instance having a valid? method, so in practice this may only work with Rails. diff --git a/cranky.gemspec b/cranky.gemspec index 40093e9..1284801 100644 --- a/cranky.gemspec +++ b/cranky.gemspec @@ -1,4 +1,7 @@ -require "lib/cranky" +lib = File.expand_path('../lib/', __FILE__) +$:.unshift lib unless $:.include?(lib) + +require "cranky/version" Gem::Specification.new do |gem| gem.name = 'cranky' diff --git a/init.rb b/init.rb index 5f2d2ba..9df7fd5 100644 --- a/init.rb +++ b/init.rb @@ -1,2 +1,2 @@ -require 'crank_it' +require 'cranky' diff --git a/lib/cranky.rb b/lib/cranky.rb index 2f0bbae..d751996 100644 --- a/lib/cranky.rb +++ b/lib/cranky.rb @@ -1,106 +1,18 @@ -class Cranky - - VERSION = "0.1.1" - -# Dir.glob("#{File.expand_path(File.dirname(__FILE__))}/*.rb").each do |file| -# require file -# # Auto include all modules found in the directory -# file =~ /.*[\/\\](.*)\.rb/ -# begin -# include $1.to_s.camelcase.constantize -# rescue -# end -# end - - attr_writer :debug - - def initialize - @what = [] - @attrs = [] - @n = 0 - end - - def build(what, attrs={}) - crank_it(what, false, attrs) - end - - def create(what, attrs={}) - crank_it(what, true, attrs) - end - - def reset - self.instance_variables.each do |var| - instance_variable_set(var, nil) - end - initialize - end - - def attributes_for(what, attrs={}) - build(what, attrs).attributes - end - - def debug(what=nil) - if block_given? - @debug = true - yield - @debug = false - elsif what - @debug = true - item = build(what) - @debug = false - item - else - @debug - end - end - - private - - def n - @n += 1 - end - - def inherit(what, attrs={}) - build(what, attrs.merge(options)) - end - - def crank_it(what, save, attrs) - @attrs << attrs; @what << what - item = self.send(what) - @attrs.pop; @what.pop - if @debug && !item.valid? - raise "Oops, the #{what} created by the Factory has the following errors: #{item.errors}" - end - item.save if save - item - end - - def define(attrs={}) - final_attrs = attrs.merge(@attrs.last) - item = get_constant(attrs[:class] ? attrs[:class] : @what.last).new - final_attrs.delete(:class) - # Assign all explicit attributes first - final_attrs.each do |attr, value| - item.send("#{attr}=", value) if item.respond_to?("#{attr}=") && !value.respond_to?("call") - end - # Then call any blocks - final_attrs.each do |attr, value| - item.send("#{attr}=", value.call(item)) if item.respond_to?("#{attr}=") && value.respond_to?("call") - end - item - end - - # Nicked from here: http://gist.github.com/301173 - def get_constant(name_sym) - name = name_sym.to_s.split('_').collect {|s| s.capitalize }.join('') - Object.const_defined?(name) ? Object.const_get(name) : Object.const_missing(name) - end - - def options - @attrs.last - end - +require 'cranky/version' +require 'cranky/job' +require 'cranky/factory' + +# Instantiate a factory, this enables an easy drop in for tests written for Factory Girl +Factory = Cranky::Factory.new unless defined?(Factory) + +# Alternative Cranky specific syntax: +# crank(:user) # equivalent to Factory.build(:user) +# crank!(:user) # equivalent to Factory.create(:user) +def crank(*args) + Factory.build(*args) end -Factory = Cranky.new unless defined?(Factory) +def crank!(*args) + Factory.create(*args) +end diff --git a/lib/cranky/factory.rb b/lib/cranky/factory.rb new file mode 100644 index 0000000..975a3e2 --- /dev/null +++ b/lib/cranky/factory.rb @@ -0,0 +1,98 @@ +module Cranky + class Factory + + attr_writer :debug + + def initialize + # Factory jobs can be nested, i.e. a factory method can itself invoke another factory method to + # build a dependent object. In this case jobs the jobs are pushed into a pipeline and executed + # in a last in first out order. + @pipeline = [] + @n = 0 + @errors = [] + end + + def build(what, overrides={}) + crank_it(what, overrides) + end + + def create(what, overrides={}) + item = build(what, overrides) + item.save + item + end + + # Reset the factory instance, clear all instance variables + def reset + self.instance_variables.each do |var| + instance_variable_set(var, nil) + end + initialize + end + + def attributes_for(what, attrs={}) + build(what, attrs).attributes + end + + # Can be left in your tests as an alternative to build and to warn if your factory method + # ever starts producing invalid instances + def debug(*args) + item = build(*args) + if !item.valid? + raise "Oops, the #{item.class} created by the Factory has the following errors: #{item.errors}" + end + end + + # Same thing for create + def debug!(*args) + item = debug + item.save + item + end + + private + + def n + @n += 1 + end + + def inherit(what, overrides={}) + build(what, overrides.merge(options)) + end + + # Execute the requested factory method, crank out the target object! + def crank_it(what, overrides) + item = "TBD" + new_job(what, overrides) do + item = self.send(what) # Invoke the factory method + end + item + end + + # This method actually makes the required object instance, it gets called by the users factory + # method, where the name 'define' makes more sense than it does here! + def define(defaults={}) + current_job.defaults = defaults + current_job.execute + end + + def current_job + @pipeline.last + end + + # Returns a hash containing any top-level overrides passed in when the current factory was invoked + def options + current_job.overrides + end + + # Adds a new job to the pipeline then yields to the caller to execute it + def new_job(what, overrides) + @pipeline << Job.new(what, overrides) + yield + @pipeline.pop + end + + end + +end + diff --git a/lib/cranky/job.rb b/lib/cranky/job.rb new file mode 100644 index 0000000..80af64f --- /dev/null +++ b/lib/cranky/job.rb @@ -0,0 +1,44 @@ +module Cranky + class Job + + attr_writer :defaults + attr_reader :overrides + + def initialize(target, overrides={}) + @defaults = {} + @target = target + @overrides = overrides + end + + def attributes + @attributes ||= @defaults.merge(@overrides) + end + + def defaults=(defs) + @attributes = nil # Needs to be re-calculated + @defaults = defs + end + + def execute + item = get_constant(attributes[:class] ? attributes[:class] : @target).new + # Assign all explicit attributes first + attributes.each do |attribute, value| + item.send("#{attribute}=", value) if item.respond_to?("#{attribute}=") && !value.respond_to?("call") + end + # Then call any blocks + attributes.each do |attribute, value| + item.send("#{attribute}=", value.call(item)) if item.respond_to?("#{attribute}=") && value.respond_to?("call") + end + item + end + + private + + # Nicked from here: http://gist.github.com/301173 + def get_constant(name_sym) + name = name_sym.to_s.split('_').collect {|s| s.capitalize }.join('') + Object.const_defined?(name) ? Object.const_get(name) : Object.const_missing(name) + end + + end +end diff --git a/lib/cranky/version.rb b/lib/cranky/version.rb new file mode 100644 index 0000000..5142483 --- /dev/null +++ b/lib/cranky/version.rb @@ -0,0 +1,3 @@ +module Cranky + VERSION = "0.2.0" +end diff --git a/rakefile b/rakefile index a937298..bb7cc07 100644 --- a/rakefile +++ b/rakefile @@ -1,4 +1,6 @@ -require 'lib/cranky' +$:.unshift File.expand_path("../lib", __FILE__) + +require 'cranky' require 'rubygems' require 'spec/rake/spectask' diff --git a/spec/cranky_spec.rb b/spec/cranky_spec.rb index f3ba1ab..c87954b 100644 --- a/spec/cranky_spec.rb +++ b/spec/cranky_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Cranky do +describe "The Cranky factory" do before(:each) do end @@ -54,6 +54,13 @@ b.role.should == :admin end + it "should give top priority to attributes defined at the top level, even when inheriting" do + a = Factory.build(:admin_manually, :role => :something_else) + a.role.should == :something_else + b = Factory.build(:admin_by_define, :role => :something_else) + b.role.should == :something_else + end + it "should create unique values using the n method" do a = Factory.build(:user) b = Factory.build(:user) @@ -66,33 +73,6 @@ describe "debugger" do it "should raise an error if the factory produces an invalid object when enabled (rails only)" do - Factory.debug = true - error = false - begin - Factory.build(:user) - rescue - error = true - end - error.should == true - Factory.debug = false - end - - it "can be run as a block" do - Factory.debug.should == false - error = false - Factory.debug do - begin - Factory.build(:user) - rescue - error = true - end - end - error.should == true - Factory.debug.should == false - end - - it "can be run inline" do - Factory.debug.should == false error = false begin Factory.debug(:user) @@ -115,6 +95,20 @@ Factory.build(:user, :name => "jenny", :email => Proc.new{ |u| "#{u.name}@home.com" }).email.should == "jenny@home.com" Factory.build(:user, :name => Proc.new{"jimmy" + " cranky"}).name.should == "jimmy cranky" end + + it "allows factories to call other factories" do + Factory.build(:user_manually).address.city.should == "New York" + Factory.create(:user_manually).address.city.should == "New York" + Factory.create(:user_manually).address.saved?.should == false + Factory.build(:user_by_define).address.city.should == "New York" + Factory.create(:user_by_define).address.city.should == "New York" + Factory.create(:user_by_define).address.saved?.should == true + end + + it "should also have its own syntax" do + crank(:user).saved?.should == false + crank!(:address).saved?.should == true + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3679cca..b6af0e1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,14 +2,8 @@ require 'spec' require 'cranky' -# A basic model to crank out -class User - attr_accessor :name - attr_accessor :role - attr_accessor :email - attr_accessor :unique - attr_accessor :argument_received +class TestClass def save @saved = true end @@ -29,11 +23,26 @@ def errors def attributes self.instance_variables end +end + +# A basic model to crank out +class User < TestClass + attr_accessor :name + attr_accessor :role + attr_accessor :email + attr_accessor :unique + attr_accessor :argument_received + attr_accessor :address +end + +class Address < TestClass + attr_accessor :address + attr_accessor :city end # Some basic factory methods -class Cranky +class Cranky::Factory attr_accessor :some_instance_variable @@ -43,6 +52,7 @@ def user_manually u.role = options[:role] || :user u.unique = "value#{n}" u.email = "fred@home.com" + u.address = Factory.build(:address) u end @@ -51,7 +61,8 @@ def user_by_define :name => "Fred", :role => :user, :unique => "value#{n}", - :email => "fred@home.com" + :email => "fred@home.com", + :address => Factory.create(:address) u.argument_received = true if options[:argument_supplied] u end @@ -65,6 +76,11 @@ def admin_by_define inherit(:user_by_define, :role => :admin) end + def address + define :address => "25 Wisteria Lane", + :city => "New York" + end + end