Skip to content

Commit

Permalink
Optionally disable duplicate assignment of attributes in initialize_with
Browse files Browse the repository at this point in the history
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
  • Loading branch information
joshuaclayton committed May 18, 2012
1 parent 64e50a3 commit a5b3a97
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 3 deletions.
9 changes: 9 additions & 0 deletions GETTING_STARTED.md
Expand Up @@ -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
-----------------

Expand Down
6 changes: 5 additions & 1 deletion lib/factory_girl.rb
Expand Up @@ -37,6 +37,9 @@
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
Expand All @@ -50,7 +53,8 @@ def self.reset_configuration

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)
Expand Down
24 changes: 22 additions & 2 deletions lib/factory_girl/attribute_assigner.rb
Expand Up @@ -30,8 +30,28 @@ def hash

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
Expand All @@ -43,7 +63,7 @@ def get(attribute_name)
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
Expand Down
4 changes: 4 additions & 0 deletions lib/factory_girl/configuration.rb
Expand Up @@ -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'))
Expand All @@ -11,6 +13,8 @@ def initialize
@callback_names = Set.new
@definition = Definition.new

@duplicate_attribute_assignment_from_initialize_with = true

to_create {|instance| instance.save! }
initialize_with { new }
end
Expand Down
17 changes: 17 additions & 0 deletions lib/factory_girl/decorator.rb
@@ -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
9 changes: 9 additions & 0 deletions lib/factory_girl/decorator/invocation_ignorer.rb
@@ -0,0 +1,9 @@
module FactoryGirl
class Decorator
class InvocationIgnorer < Decorator
def __invoked_methods__
[]
end
end
end
end
19 changes: 19 additions & 0 deletions lib/factory_girl/decorator/invocation_tracker.rb
@@ -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
2 changes: 2 additions & 0 deletions spec/acceptance/global_initialize_with_spec.rb
Expand Up @@ -2,6 +2,8 @@

describe 'global initialize_with' do
before do
ActiveSupport::Deprecation.silenced = true

define_class('User') do
attr_accessor:name

Expand Down
47 changes: 47 additions & 0 deletions spec/acceptance/initialize_with_spec.rb
Expand Up @@ -4,6 +4,8 @@
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)
Expand All @@ -26,6 +28,8 @@ def self.construct(name, age)
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")
Expand All @@ -51,6 +55,8 @@ def self.construct(name)
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")
Expand All @@ -75,6 +81,8 @@ def self.construct(name)
include FactoryGirl::Syntax::Methods

before do
ActiveSupport::Deprecation.silenced = true

define_class("ReportGenerator") do
attr_reader :name, :data

Expand Down Expand Up @@ -108,6 +116,8 @@ def initialize(name, data)

describe "initialize_with parent and child factories" do
before do
ActiveSupport::Deprecation.silenced = true

define_class("Awesome") do
attr_reader :name

Expand Down Expand Up @@ -148,6 +158,8 @@ def initialize(name)

describe "initialize_with implicit constructor" do
before do
ActiveSupport::Deprecation.silenced = true

define_class("Awesome") do
attr_reader :name

Expand All @@ -171,3 +183,38 @@ def initialize(name)
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
2 changes: 2 additions & 0 deletions spec/acceptance/traits_spec.rb
Expand Up @@ -490,6 +490,8 @@

describe "traits with initialize_with" do
before do
ActiveSupport::Deprecation.silenced = true

define_class("User") do
attr_reader :name

Expand Down

0 comments on commit a5b3a97

Please sign in to comment.