From 578036480fd98332259fb42ce11ad4933a0c0f2b Mon Sep 17 00:00:00 2001 From: Joshua Clayton Date: Fri, 20 Jan 2012 13:04:48 -0500 Subject: [PATCH] Implement initialize_with to allow overriding object instantiation Factory Girl now allows factories to override object instantiation. This means factories can use factory methods (e.g. methods other than new) as well as pass arguments explicitly. factory :user do ignore do things { ["thing 1", "thing 2"] } end initialize_with { User.construct_with_things(things) } end factory :report_generator do ignore do name { "Generic Report" } data { {:foo => "bar", :baz => "buzz"} } end initialize_with { ReportGenerator.new(name, data) } end Whitespace Code recommendations --- GETTING_STARTED.md | 38 +++++- lib/factory_girl/attribute_assigner.rb | 6 +- lib/factory_girl/definition.rb | 7 +- lib/factory_girl/definition_proxy.rb | 4 + lib/factory_girl/factory.rb | 11 +- lib/factory_girl/null_factory.rb | 2 +- spec/acceptance/initialize_with_spec.rb | 147 +++++++++++++++++++++ spec/factory_girl/definition_proxy_spec.rb | 11 ++ spec/factory_girl/null_factory_spec.rb | 1 + 9 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 spec/acceptance/initialize_with_spec.rb diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 2f3fe98..1d58505 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -590,6 +590,42 @@ To set the attributes for each of the factories, you can pass in a hash as you n twenty_year_olds = FactoryGirl.build_list(:user, 25, :date_of_birth => 20.years.ago) ``` +Custom Construction +------------------- + +Instantiating objects can be overridden in the case where you'd rather not +call `new` on your build class or you have some other factory method that +you'd prefer to use. Using custom construction also allows for your objects to +be instantiated with any number of arguments. + +```ruby +# user.rb +class User + attr_accessor :name, :email + + def initialize(name) + @name = name + end +end + +# factories.rb +sequence(:name) {|n| "person#{n}@example.com" } + +factory :user do + ignore do + name { Faker::Name.name } + end + + email + initialize_with { User.new(name) } +end + +FactoryGirl.build(:user).name # Bob Hope +``` + +Notice that I ignored the `name` attribute. If you don't want attributes +reassigned after your object has been instantiated, you'll want to `ignore` them. + Cucumber Integration -------------------- @@ -620,4 +656,4 @@ User.blueprint do end User.make(:name => 'Johnny') -``` \ No newline at end of file +``` diff --git a/lib/factory_girl/attribute_assigner.rb b/lib/factory_girl/attribute_assigner.rb index b93b510..701f63e 100644 --- a/lib/factory_girl/attribute_assigner.rb +++ b/lib/factory_girl/attribute_assigner.rb @@ -1,7 +1,7 @@ module FactoryGirl class AttributeAssigner - def initialize(build_class, evaluator) - @build_class = build_class + def initialize(evaluator, &instance_builder) + @instance_builder = instance_builder @evaluator = evaluator @attribute_list = evaluator.class.attribute_list @attribute_names_assigned = [] @@ -29,7 +29,7 @@ module FactoryGirl private def build_class_instance - @build_class_instance ||= @build_class.new + @build_class_instance ||= @evaluator.instance_exec(&@instance_builder) end def get(attribute_name) diff --git a/lib/factory_girl/definition.rb b/lib/factory_girl/definition.rb index 222773c..4cf3564 100644 --- a/lib/factory_girl/definition.rb +++ b/lib/factory_girl/definition.rb @@ -1,6 +1,6 @@ module FactoryGirl class Definition - attr_reader :callbacks, :defined_traits, :declarations + attr_reader :callbacks, :defined_traits, :declarations, :constructor def initialize(name = nil, base_traits = []) @declarations = DeclarationList.new(name) @@ -9,6 +9,7 @@ module FactoryGirl @to_create = lambda {|instance| instance.save! } @base_traits = base_traits @additional_traits = [] + @constructor = nil end delegate :declare_attribute, :to => :declarations @@ -50,6 +51,10 @@ module FactoryGirl @defined_traits << trait end + def define_constructor(&block) + @constructor = block + end + private def base_traits diff --git a/lib/factory_girl/definition_proxy.rb b/lib/factory_girl/definition_proxy.rb index ad2e5c4..72e4af5 100644 --- a/lib/factory_girl/definition_proxy.rb +++ b/lib/factory_girl/definition_proxy.rb @@ -161,5 +161,9 @@ module FactoryGirl def trait(name, &block) @definition.define_trait(Trait.new(name, &block)) end + + def initialize_with(&block) + @definition.define_constructor(&block) + end end end diff --git a/lib/factory_girl/factory.rb b/lib/factory_girl/factory.rb index c3e7d1d..c457e2f 100644 --- a/lib/factory_girl/factory.rb +++ b/lib/factory_girl/factory.rb @@ -43,7 +43,7 @@ module FactoryGirl proxy = proxy_class.new evaluator = evaluator_class.new(proxy, overrides.symbolize_keys) - attribute_assigner = AttributeAssigner.new(build_class, evaluator) + attribute_assigner = AttributeAssigner.new(evaluator, &instance_builder) proxy.result(attribute_assigner, to_create).tap(&block) end @@ -123,6 +123,10 @@ module FactoryGirl processing_order.map {|factory| factory.callbacks }.flatten end + def constructor + @constructor ||= @definition.constructor || parent.constructor + end + private def assert_valid_options(options) @@ -143,6 +147,11 @@ module FactoryGirl end end + def instance_builder + build_class = self.build_class + constructor || lambda { build_class.new } + end + def initialize_copy(source) super @definition = @definition.clone diff --git a/lib/factory_girl/null_factory.rb b/lib/factory_girl/null_factory.rb index 2a7d9a7..5e81246 100644 --- a/lib/factory_girl/null_factory.rb +++ b/lib/factory_girl/null_factory.rb @@ -6,7 +6,7 @@ module FactoryGirl @definition = Definition.new end - delegate :defined_traits, :callbacks, :attributes, :to => :definition + delegate :defined_traits, :callbacks, :attributes, :constructor, :to => :definition def compile; end def class_name; end diff --git a/spec/acceptance/initialize_with_spec.rb b/spec/acceptance/initialize_with_spec.rb new file mode 100644 index 0000000..a5305ed --- /dev/null +++ b/spec/acceptance/initialize_with_spec.rb @@ -0,0 +1,147 @@ +require "spec_helper" + +describe "initialize_with with non-FG attributes" do + include FactoryGirl::Syntax::Methods + + before do + define_model("User", :name => :string, :age => :integer) do + def self.construct(name, age) + new(:name => name, :age => age) + end + end + + FactoryGirl.define do + factory :user do + initialize_with { User.construct("John Doe", 21) } + end + end + end + + subject { build(:user) } + its(:name) { should == "John Doe" } + its(:age) { should == 21 } +end + +describe "initialize_with with FG attributes that are ignored" do + include FactoryGirl::Syntax::Methods + + before do + define_model("User", :name => :string) do + def self.construct(name) + new(:name => "#{name} from .construct") + end + end + + FactoryGirl.define do + factory :user do + ignore do + name { "Handsome Chap" } + end + + initialize_with { User.construct(name) } + end + end + end + + subject { build(:user) } + its(:name) { should == "Handsome Chap from .construct" } +end + +describe "initialize_with with FG attributes that are not ignored" do + include FactoryGirl::Syntax::Methods + + before do + define_model("User", :name => :string) do + def self.construct(name) + new(:name => "#{name} from .construct") + end + end + + FactoryGirl.define do + factory :user do + name { "Handsome Chap" } + + initialize_with { User.construct(name) } + end + end + end + + it "assigns each attribute even if the attribute has been used in the constructor" do + build(:user).name.should == "Handsome Chap" + end +end + +describe "initialize_with non-ORM-backed objects" do + include FactoryGirl::Syntax::Methods + + before do + define_class("ReportGenerator") do + attr_reader :name, :data + + def initialize(name, data) + @name = name + @data = data + end + end + + FactoryGirl.define do + sequence(:random_data) { 5.times.map { Kernel.rand(200) } } + + factory :report_generator do + ignore do + name "My Awesome Report" + end + + initialize_with { ReportGenerator.new(name, FactoryGirl.generate(:random_data)) } + end + end + end + + it "allows for overrides" do + build(:report_generator, :name => "Overridden").name.should == "Overridden" + end + + it "generates random data" do + build(:report_generator).data.length.should == 5 + end +end + +describe "initialize_with parent and child factories" do + before do + define_class("Awesome") do + attr_reader :name + + def initialize(name) + @name = name + end + end + + FactoryGirl.define do + factory :awesome do + ignore do + name "Great" + end + + initialize_with { Awesome.new(name) } + + factory :sub_awesome do + ignore do + name "Sub" + end + end + + factory :super_awesome do + initialize_with { Awesome.new("Super") } + end + end + end + end + + it "uses the parent's constructor when the child factory doesn't assign it" do + FactoryGirl.build(:sub_awesome).name.should == "Sub" + end + + it "allows child factories to override initialize_with" do + FactoryGirl.build(:super_awesome).name.should == "Super" + end +end diff --git a/spec/factory_girl/definition_proxy_spec.rb b/spec/factory_girl/definition_proxy_spec.rb index ddd3b92..d5870f2 100644 --- a/spec/factory_girl/definition_proxy_spec.rb +++ b/spec/factory_girl/definition_proxy_spec.rb @@ -184,3 +184,14 @@ describe FactoryGirl::DefinitionProxy, "#trait" do subject.should have_trait(:male).with_block(male_trait) end end + +describe FactoryGirl::DefinitionProxy, "#initialize_with" do + subject { FactoryGirl::Definition.new } + let(:proxy) { FactoryGirl::DefinitionProxy.new(subject) } + + it "defines the constructor on the definition" do + constructor = Proc.new { Array.new } + proxy.initialize_with(&constructor) + subject.constructor.should == constructor + end +end diff --git a/spec/factory_girl/null_factory_spec.rb b/spec/factory_girl/null_factory_spec.rb index ead5614..f86ed61 100644 --- a/spec/factory_girl/null_factory_spec.rb +++ b/spec/factory_girl/null_factory_spec.rb @@ -4,6 +4,7 @@ describe FactoryGirl::NullFactory do it { should delegate(:defined_traits).to(:definition) } it { should delegate(:callbacks).to(:definition) } it { should delegate(:attributes).to(:definition) } + it { should delegate(:constructor).to(:definition) } its(:compile) { should be_nil } its(:class_name) { should be_nil }