Optionally disable duplicate assignment of attributes in initialize_with

By setting:

    FactoryGirl.duplicate_attribute_assignment_from_initialize_with =
false

This turns off duplicate assignment of attributes accessed from
initialize_with. This means any attributes accessed from the factory and
assigned in the initialize_with block won't be subsequently set after
the object has been instantiated.

This will be the default functionality in 4.0.

Closes #345
This commit is contained in:
Joshua Clayton 2012-05-18 14:28:51 -04:00
parent 64e50a3121
commit a5b3a97c9d
10 changed files with 136 additions and 3 deletions

View File

@ -771,6 +771,15 @@ FactoryGirl.define do
end
```
When using `initialize_with`, attributes accessed from the `initialize_with`
block are assigned a second time to the instance. Duplicate assignment can be
disabled by configuring FactoryGirl as such:
FactoryGirl.duplicate_attribute_assignment_from_initialize_with = false
This would allow for attributes declared as ignored to not be ignored, since
it won't assign them for a second time.
Custom Strategies
-----------------

View File

@ -37,6 +37,9 @@ require 'factory_girl/syntax'
require 'factory_girl/syntax_runner'
require 'factory_girl/find_definitions'
require 'factory_girl/reload'
require 'factory_girl/decorator'
require 'factory_girl/decorator/invocation_tracker'
require 'factory_girl/decorator/invocation_ignorer'
require 'factory_girl/version'
module FactoryGirl
@ -50,7 +53,8 @@ module FactoryGirl
class << self
delegate :factories, :sequences, :traits, :strategies, :callback_names,
:to_create, :skip_create, :initialize_with, :constructor, to: :configuration
:to_create, :skip_create, :initialize_with, :constructor, :duplicate_attribute_assignment_from_initialize_with,
:duplicate_attribute_assignment_from_initialize_with=, to: :configuration
end
def self.register_factory(factory)

View File

@ -30,8 +30,28 @@ module FactoryGirl
private
def method_tracking_evaluator
@method_tracking_evaluator ||= invocation_decorator.new(@evaluator)
end
def invocation_decorator
if FactoryGirl.duplicate_attribute_assignment_from_initialize_with
Decorator::InvocationIgnorer
else
Decorator::InvocationTracker
end
end
def methods_invoked_on_evaluator
@method_tracking_evaluator.__invoked_methods__
end
def build_class_instance
@build_class_instance ||= @evaluator.instance_exec(&@instance_builder)
@build_class_instance ||= method_tracking_evaluator.instance_exec(&@instance_builder).tap do
if @instance_builder != FactoryGirl.constructor && FactoryGirl.duplicate_attribute_assignment_from_initialize_with
ActiveSupport::Deprecation.warn 'Accessing attributes from initialize_with when duplicate assignment is enabled is deprecated; use FactoryGirl.duplicate_attribute_assignment_from_initialize_with = false.', caller
end
end
end
def build_hash
@ -43,7 +63,7 @@ module FactoryGirl
end
def attributes_to_set_on_instance
(attribute_names_to_assign - @attribute_names_assigned).uniq
(attribute_names_to_assign - @attribute_names_assigned - methods_invoked_on_evaluator).uniq
end
def attributes_to_set_on_hash

View File

@ -3,6 +3,8 @@ module FactoryGirl
class Configuration
attr_reader :factories, :sequences, :traits, :strategies, :callback_names
attr_accessor :duplicate_attribute_assignment_from_initialize_with
def initialize
@factories = DisallowsDuplicatesRegistry.new(Registry.new('Factory'))
@sequences = DisallowsDuplicatesRegistry.new(Registry.new('Sequence'))
@ -11,6 +13,8 @@ module FactoryGirl
@callback_names = Set.new
@definition = Definition.new
@duplicate_attribute_assignment_from_initialize_with = true
to_create {|instance| instance.save! }
initialize_with { new }
end

View File

@ -0,0 +1,17 @@
module FactoryGirl
class Decorator < BasicObject
undef_method :==
def initialize(component)
@component = component
end
def method_missing(name, *args, &block)
@component.send(name, *args, &block)
end
def send(symbol, *args)
__send__(symbol, *args)
end
end
end

View File

@ -0,0 +1,9 @@
module FactoryGirl
class Decorator
class InvocationIgnorer < Decorator
def __invoked_methods__
[]
end
end
end
end

View File

@ -0,0 +1,19 @@
module FactoryGirl
class Decorator
class InvocationTracker < Decorator
def initialize(component)
super
@invoked_methods = []
end
def method_missing(name, *args, &block)
@invoked_methods << name
super
end
def __invoked_methods__
@invoked_methods.uniq
end
end
end
end

View File

@ -2,6 +2,8 @@ require 'spec_helper'
describe 'global initialize_with' do
before do
ActiveSupport::Deprecation.silenced = true
define_class('User') do
attr_accessor:name

View File

@ -4,6 +4,8 @@ describe "initialize_with with non-FG attributes" do
include FactoryGirl::Syntax::Methods
before do
ActiveSupport::Deprecation.silenced = true
define_model("User", name: :string, age: :integer) do
def self.construct(name, age)
new(name: name, age: age)
@ -26,6 +28,8 @@ describe "initialize_with with FG attributes that are ignored" do
include FactoryGirl::Syntax::Methods
before do
ActiveSupport::Deprecation.silenced = true
define_model("User", name: :string) do
def self.construct(name)
new(name: "#{name} from .construct")
@ -51,6 +55,8 @@ describe "initialize_with with FG attributes that are not ignored" do
include FactoryGirl::Syntax::Methods
before do
ActiveSupport::Deprecation.silenced = true
define_model("User", name: :string) do
def self.construct(name)
new(name: "#{name} from .construct")
@ -75,6 +81,8 @@ describe "initialize_with non-ORM-backed objects" do
include FactoryGirl::Syntax::Methods
before do
ActiveSupport::Deprecation.silenced = true
define_class("ReportGenerator") do
attr_reader :name, :data
@ -108,6 +116,8 @@ end
describe "initialize_with parent and child factories" do
before do
ActiveSupport::Deprecation.silenced = true
define_class("Awesome") do
attr_reader :name
@ -148,6 +158,8 @@ end
describe "initialize_with implicit constructor" do
before do
ActiveSupport::Deprecation.silenced = true
define_class("Awesome") do
attr_reader :name
@ -171,3 +183,38 @@ describe "initialize_with implicit constructor" do
FactoryGirl.build(:awesome, name: "Awesome name").name.should == "Awesome name"
end
end
describe "initialize_with doesn't duplicate assignment on attributes accessed from initialize_with" do
before do
ActiveSupport::Deprecation.silenced = true
define_class("User") do
attr_reader :name
attr_accessor :email
def initialize(name)
@name = name
end
end
FactoryGirl.define do
sequence(:email) {|n| "person#{n}@example.com" }
factory :user do
email
name { email.gsub(/\@.+/, "") }
initialize_with { new(name) }
end
end
end
it "instantiates the correct object" do
FactoryGirl.duplicate_attribute_assignment_from_initialize_with = false
built_user = FactoryGirl.build(:user)
built_user.name.should == "person1"
built_user.email.should == "person1@example.com"
end
end

View File

@ -490,6 +490,8 @@ end
describe "traits with initialize_with" do
before do
ActiveSupport::Deprecation.silenced = true
define_class("User") do
attr_reader :name