Skip to content

Commit

Permalink
Switch to unified factory/fixture approach.
Browse files Browse the repository at this point in the history
  • Loading branch information
jbarnette committed Sep 11, 2011
1 parent 446514e commit b5511a5
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 136 deletions.
183 changes: 72 additions & 111 deletions lib/modelizer.rb
Original file line number Diff line number Diff line change
@@ -1,138 +1,99 @@
require "modelizer/assertions"
require "modelizer/validations"
require "zlib"

module Modelizer
def build name, overrides = nil, &block
model, *initializers = Modelizer.factories[name]
raise "Can't find the \"#{name}\" factory." unless model

# Duh.
VERSION = "4.0.0"
obj = model.new

include Modelizer::Assertions
initializers << block if block_given?
initializers.each { |i| instance_exec obj, &i }

# Test classes that should be considered abstract when rendering
# tests for a model template.
overrides.each { |k, v| obj.send "#{k}=", v } if overrides

obj
end

TEST_CLASSES = []
def create name, overrides = nil, &block
obj = build name, overrides, &block

%w(Test::Unit::TestCase Minitest::Unit::TestCase
ActiveSupport::TestCase).each do |k|
obj.save!

TEST_CLASSES <<
k.split("::").inject(Object) { |a, b| a.const_get b } rescue nil
obj
end

@@cache = {}
def self.cache; @@cache end
def use name
model, id = Modelizer.ids[name]
raise "Can't find the \"#{name}\" fixture." unless model

def self.included target
target.extend ClassMethods
target.extend Modelizer::Validations
model.find id
end

def self.method_name_for model_class
underscore model_class.name
class Context < Struct.new(:instances)
def identify name
Modelizer.identify name
end

def use name
instances[name] or raise "Can't find the \"#{name}\" fixture."
end
end

def self.model_class_for test_class
test_class.name.gsub(/Test$/, "").constantize
def self.included klass
Dir[glob].sort.each { |f| instance_eval File.read(f), f, 1 }

instances = {}
context = Context.new instances

fixtures.each do |name, value|
instances[name] = value.first.new
end

instances.each do |name, obj|
_, *initializers = fixtures[name]
initializers.each { |i| context.instance_exec obj, &i }

obj.id = identify name
ids[name] = [obj.class, obj.id]
end

ActiveRecord::Base.transaction do
instances.each { |_, obj| obj.save! }
end
end

def self.underscore classname
classname.gsub(/::/, '_').
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
gsub(/([a-z\d])([A-Z])/,'\1_\2').
tr("-", "_").
downcase
class << self
attr_accessor :glob
end

def assign_model_template_attributes model, attributes
model.assign_attributes attributes, without_protection: true
model
self.glob = "test/{factories,fixtures}/**/*.rb"

def self.cache
@cache ||= {}
end

def valid_model_template_attributes klass, extras = {}
defaults, block = ::Modelizer.cache[klass]
lazy = block && instance_eval(&block)
[defaults, lazy, extras].compact.inject { |t, s| t.merge s }
def self.factory name, model, &initializer
factories[name] = [model, initializer]
end

def valid_model_template_attributes_without klass, excluded
valid_model_template_attributes(klass).delete_if do |k, v|
excluded.include? k
end
def self.factories
@factories ||= {}
end

module ClassMethods
def model_template_for klass, defaults = {}, &block
if defaults.nil? && !block
raise ArgumentError, "default attributes or lazy block required"
end

::Modelizer.cache[klass] = [defaults, block]

model = ::Modelizer.method_name_for klass
klass = klass.name

module_eval <<-END, __FILE__, __LINE__ + 1
def valid_#{model}_attributes extras = {}
valid_model_template_attributes #{klass}, extras
end
def valid_#{model}_attributes_without *excluded
valid_model_template_attributes_without #{klass}, excluded
end
def new_#{model} extras = {}
assign_model_template_attributes #{klass}.new,
valid_model_template_attributes(#{klass}, extras)
end
def new_#{model}_without *excluded
assign_model_template_attributes #{klass}.new,
valid_model_template_attributes_without(#{klass}, excluded)
end
def create_#{model} extras = {}
(m = new_#{model}(extras)).save; m
end
def create_#{model}! extras = {}
(m = new_#{model}(extras)).save!; m
end
def create_#{model}_without *excluded
(m = new_#{model}_without(*excluded)).save; m
end
def create_#{model}_without! *excluded
(m = new_#{model}_without(*excluded)).save!; m
end
END

# Install a test that ensures the model template is valid. If
# the template is defined in one of the abstract test
# superclasses, generate a whole new testcase. If it's in a
# concrete test, just generate a method.

file, line = caller.first.split ":"
line = line.to_i

test = <<-END
def test_model_template_for_#{model}
assert (m = new_#{model}).valid?,
"#{klass} template is invalid: " +
m.errors.full_messages.to_sentence
end
END

if TEST_CLASSES.include? self
eval <<-END, nil, file, line - 2
class ::ModelTemplateFor#{klass}Test < ActiveSupport::TestCase
#{test}
end
END
else
module_eval test, file, line - 1
end
end
def self.fixture name, model, &initializer
fixtures[name] = [model, initializer]
end

def self.fixtures
@fixtures ||= {}
end

def self.identify name
Zlib.crc32(name.to_s) % (2 ** 30 - 1)
end

def self.ids
@ids ||= {}
end
end
2 changes: 2 additions & 0 deletions lib/modelizer/assertions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ def assert_invalid attribute, model, match = nil
assert_match match, model.errors.on(attribute) if match
end
end

include Assertions
end
37 changes: 12 additions & 25 deletions lib/modelizer/validations.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,23 @@
module Modelizer
module Validations
def test_validations_for attribute, *validations
@klass ||= ::Modelizer.model_class_for self
@model ||= ::Modelizer.method_name_for @klass

unless instance_methods.collect { |m| m.to_s }.include? "new_#{@model}"
raise "no model template for #{@klass.name}"
end

# FIX: location in original test file

validations.each do |v|
test = send "validation_lambda_for_#{v}", @klass, @model, attribute
define_method "test_#{attribute}_#{v}", &test
def test_presence_for plan, attribute
define_method "test_#{attribute}_presence" do
bad = build plan, attribute => nil
assert_invalid attribute, bad
end
end

private

def validation_lambda_for_presence klass, model, attribute
lambda do
assert_invalid attribute, send("new_#{model}", attribute => nil)
def test_uniqueness_for plan, attribute
define_method "test_#{attribute}_uniqueness" do
good = create plan
bad = build(plan) { |o| o.send("#{attribute}=", good.send(attribute)) }
assert_invalid attribute, bad
end
end

def validation_lambda_for_uniqueness klass, model, attribute
lambda do
existing = klass.first
assert existing, "There's at least one #{model} fixture."

assert_invalid attribute,
send("new_#{model}", attribute => existing.send(attribute))
def test_validations_for plan, attribute, *validations
validations.each do |validation|
send "test_#{validation}_for", plan, attribute
end
end
end
Expand Down

0 comments on commit b5511a5

Please sign in to comment.