Added support for dependent attributes

This commit is contained in:
Joe Ferris 2008-05-31 17:12:33 -07:00
parent 7b3936b611
commit 05766cc8e9
5 changed files with 128 additions and 20 deletions

30
README
View File

@ -34,16 +34,20 @@ a block instead of a parameter:
u.activation_code { User.generate_activation_code }
end
== Using factories
== Dependent Attributes
# Build and save a User instance
Factory(:user)
Some attributes may need to be generated based on the values of other
attributes. This can be done by calling the attribute name on
Factory::AttributeProxy, which is yielded to lazy attribute blocks:
# Build a User instance and override the first_name property
Factory.build(:user, :first_name => 'Joe')
Factory.define :user do |u|
u.first_name 'Joe'
u.last_name 'Blow'
u.email {|a| "#{a.first_name}.#{a.last_name}@example.com".downcase }
end
# Return an attributes Hash that can be used to build a User instance
attrs = Factory.attributes_for(:user)
Factory(:user, :last_name => 'Doe').email
# => "joe.doe@example.com"
== Associations
@ -66,3 +70,15 @@ When using the association method, the same build strategy (build, create, or at
Factory.build(:post)
post.new_record? # => true
post.author.new_record # => true
== Using factories
# Build and save a User instance
Factory(:user)
# Build a User instance and override the first_name property
Factory.build(:user, :first_name => 'Joe')
# Return an attributes Hash that can be used to build a User instance
attrs = Factory.attributes_for(:user)

View File

@ -2,12 +2,13 @@ class Factory
class AttributeProxy
attr_accessor :factory, :attribute_name, :strategy #:nodoc:
attr_accessor :factory, :attribute_name, :strategy, :current_values #:nodoc:
def initialize (factory, attr, strategy) #:nodoc:
def initialize (factory, attr, strategy, values) #:nodoc:
@factory = factory
@attribute_name = attr
@strategy = strategy
@current_values = values
end
# Generates an association using the current build strategy.
@ -34,6 +35,42 @@ class Factory
Factory.send(strategy, name, attributes)
end
# Returns the value for specified attribute. A value will only be available
# if it was overridden when calling the factory, or if a value is added
# earlier in the definition of the factory.
#
# Arguments:
# attribute: (Symbol)
# The attribute whose value should be returned.
#
# Returns:
# The value of the requested attribute. (Object)
def value_for (attribute)
unless current_values.key?(attribute)
raise ArgumentError, "No such attribute: #{attribute.inspect}"
end
current_values[attribute]
end
# Undefined methods are delegated to value_for, which means that:
#
# Factory.define :user do |f|
# f.first_name 'Ben'
# f.last_name {|a| a.value_for(:first_name) }
# end
#
# and:
#
# Factory.define :user do |f|
# f.first_name 'Ben'
# f.last_name {|a| a.first_name }
# end
#
# are equivilent.
def method_missing (name, *args, &block)
current_values[name]
end
end
end

View File

@ -33,8 +33,9 @@ class Factory
@name = name
@options = options
@static_attributes = {}
@lazy_attributes = {}
@static_attributes = {}
@lazy_attribute_blocks = {}
@lazy_attribute_names = []
end
# Adds an attribute that should be assigned on generated instances for this
@ -60,7 +61,8 @@ class Factory
unless value.nil?
raise ArgumentError, "Both value and block given"
end
@lazy_attributes[name] = block
@lazy_attribute_blocks[name] = block
@lazy_attribute_names << name
else
@static_attributes[name] = value
end
@ -158,13 +160,12 @@ class Factory
private
def build_attributes_hash (override, strategy)
result = {}
@lazy_attributes.each do |name, block|
proxy = AttributeProxy.new(self, name, strategy)
result[name] = block.call(proxy) unless override.key?(name)
result = @static_attributes.merge(override)
@lazy_attribute_names.each do |name|
proxy = AttributeProxy.new(self, name, strategy, result)
result[name] = @lazy_attribute_blocks[name].call(proxy) unless override.key?(name)
end
result.update(@static_attributes)
result.update(override)
result
end
def build_instance (override, strategy)

View File

@ -7,8 +7,9 @@ class AttributeProxyTest < Test::Unit::TestCase
setup do
@factory = mock('factory')
@attr = :user
@attrs = { :first_name => 'John' }
@strategy = :create
@proxy = Factory::AttributeProxy.new(@factory, @attr, @strategy)
@proxy = Factory::AttributeProxy.new(@factory, @attr, @strategy, @attrs)
end
should "have a factory" do
@ -23,6 +24,10 @@ class AttributeProxyTest < Test::Unit::TestCase
assert_equal @strategy, @proxy.strategy
end
should "have attributes" do
assert_equal @attrs, @proxy.current_values
end
context "building an association" do
setup do
@ -44,6 +49,34 @@ class AttributeProxyTest < Test::Unit::TestCase
end
context "fetching the value of an attribute" do
setup do
@attr = :first_name
end
should "return the correct value" do
assert_equal @attrs[@attr], @proxy.value_for(@attr)
end
should "call value_for for undefined methods" do
assert_equal @attrs[@attr], @proxy.send(@attr)
end
end
context "fetching the value of an undefined attribute" do
setup do
@attr = :beachball
end
should "raise an ArgumentError" do
assert_raise(ArgumentError) { @proxy.value_for(@attr) }
end
end
end
end

View File

@ -86,6 +86,7 @@ class FactoryTest < Test::Unit::TestCase
setup do
@attr = :name
@attrs = {}
@proxy = mock('attr-proxy')
Factory::AttributeProxy.stubs(:new).returns(@proxy)
end
@ -110,7 +111,7 @@ class FactoryTest < Test::Unit::TestCase
end
should "build an attribute proxy" do
Factory::AttributeProxy.expects(:new).with(@factory, @attr, :attributes_for)
Factory::AttributeProxy.expects(:new).with(@factory, @attr, :attributes_for, @attrs)
@factory.add_attribute(@attr) {}
@factory.attributes_for
end
@ -122,6 +123,26 @@ class FactoryTest < Test::Unit::TestCase
assert_equal @proxy, yielded
end
context "when other attributes have previously been defined" do
setup do
@attr = :unimportant
@attrs = {
:one => 'whatever',
:another => 'soup'
}
@factory.add_attribute(:one, 'whatever')
@factory.add_attribute(:another) { 'soup' }
@factory.add_attribute(@attr) {}
end
should "provide previously set attributes" do
Factory::AttributeProxy.expects(:new).with(@factory, @attr, :attributes_for, @attrs)
@factory.attributes_for
end
end
end
should "add an attribute using the method name when passed an undefined method" do