Skip to content

Commit

Permalink
Significant refactoring to improve maintainability
Browse files Browse the repository at this point in the history
  • Loading branch information
Ginty committed Aug 22, 2010
1 parent 923308b commit cf1b814
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 159 deletions.
23 changes: 7 additions & 16 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand Down
5 changes: 4 additions & 1 deletion cranky.gemspec
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
2 changes: 1 addition & 1 deletion init.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
require 'crank_it'
require 'cranky'

118 changes: 15 additions & 103 deletions lib/cranky.rb
Original file line number Diff line number Diff line change
@@ -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

98 changes: 98 additions & 0 deletions lib/cranky/factory.rb
Original file line number Diff line number Diff line change
@@ -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

44 changes: 44 additions & 0 deletions lib/cranky/job.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions lib/cranky/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module Cranky
VERSION = "0.2.0"
end
4 changes: 3 additions & 1 deletion rakefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require 'lib/cranky'
$:.unshift File.expand_path("../lib", __FILE__)

require 'cranky'
require 'rubygems'
require 'spec/rake/spectask'

Expand Down
Loading

0 comments on commit cf1b814

Please sign in to comment.