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 } u.activation_code { User.generate_activation_code }
end end
== Using factories == Dependent Attributes
# Build and save a User instance Some attributes may need to be generated based on the values of other
Factory(:user) 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.define :user do |u|
Factory.build(:user, :first_name => 'Joe') 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 Factory(:user, :last_name => 'Doe').email
attrs = Factory.attributes_for(:user) # => "joe.doe@example.com"
== Associations == Associations
@ -66,3 +70,15 @@ When using the association method, the same build strategy (build, create, or at
Factory.build(:post) Factory.build(:post)
post.new_record? # => true post.new_record? # => true
post.author.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 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 @factory = factory
@attribute_name = attr @attribute_name = attr
@strategy = strategy @strategy = strategy
@current_values = values
end end
# Generates an association using the current build strategy. # Generates an association using the current build strategy.
@ -34,6 +35,42 @@ class Factory
Factory.send(strategy, name, attributes) Factory.send(strategy, name, attributes)
end 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
end end

View File

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

View File

@ -7,8 +7,9 @@ class AttributeProxyTest < Test::Unit::TestCase
setup do setup do
@factory = mock('factory') @factory = mock('factory')
@attr = :user @attr = :user
@attrs = { :first_name => 'John' }
@strategy = :create @strategy = :create
@proxy = Factory::AttributeProxy.new(@factory, @attr, @strategy) @proxy = Factory::AttributeProxy.new(@factory, @attr, @strategy, @attrs)
end end
should "have a factory" do should "have a factory" do
@ -23,6 +24,10 @@ class AttributeProxyTest < Test::Unit::TestCase
assert_equal @strategy, @proxy.strategy assert_equal @strategy, @proxy.strategy
end end
should "have attributes" do
assert_equal @attrs, @proxy.current_values
end
context "building an association" do context "building an association" do
setup do setup do
@ -44,6 +49,34 @@ class AttributeProxyTest < Test::Unit::TestCase
end 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
end end

View File

@ -86,6 +86,7 @@ class FactoryTest < Test::Unit::TestCase
setup do setup do
@attr = :name @attr = :name
@attrs = {}
@proxy = mock('attr-proxy') @proxy = mock('attr-proxy')
Factory::AttributeProxy.stubs(:new).returns(@proxy) Factory::AttributeProxy.stubs(:new).returns(@proxy)
end end
@ -110,7 +111,7 @@ class FactoryTest < Test::Unit::TestCase
end end
should "build an attribute proxy" do 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.add_attribute(@attr) {}
@factory.attributes_for @factory.attributes_for
end end
@ -122,6 +123,26 @@ class FactoryTest < Test::Unit::TestCase
assert_equal @proxy, yielded assert_equal @proxy, yielded
end 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 end
should "add an attribute using the method name when passed an undefined method" do should "add an attribute using the method name when passed an undefined method" do