diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 2f3fe98c2..1d5850508 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 b93b51094..701f63e3a 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 @@ def hash 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 222773c52..4cf3564a4 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 @@ def initialize(name = nil, base_traits = []) @to_create = lambda {|instance| instance.save! } @base_traits = base_traits @additional_traits = [] + @constructor = nil end delegate :declare_attribute, :to => :declarations @@ -50,6 +51,10 @@ def define_trait(trait) @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 ad2e5c4d8..72e4af53b 100644 --- a/lib/factory_girl/definition_proxy.rb +++ b/lib/factory_girl/definition_proxy.rb @@ -161,5 +161,9 @@ def factory(name, options = {}, &block) 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 c3e7d1d2e..c457e2fe7 100644 --- a/lib/factory_girl/factory.rb +++ b/lib/factory_girl/factory.rb @@ -43,7 +43,7 @@ def run(proxy_class, overrides, &block) #:nodoc: 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 @@ def callbacks 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 @@ def parent 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 2a7d9a77d..5e812461c 100644 --- a/lib/factory_girl/null_factory.rb +++ b/lib/factory_girl/null_factory.rb @@ -6,7 +6,7 @@ def initialize @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 000000000..a5305ed4e --- /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 ddd3b92b8..d5870f2e8 100644 --- a/spec/factory_girl/definition_proxy_spec.rb +++ b/spec/factory_girl/definition_proxy_spec.rb @@ -184,3 +184,14 @@ 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 ead5614ef..f86ed6128 100644 --- a/spec/factory_girl/null_factory_spec.rb +++ b/spec/factory_girl/null_factory_spec.rb @@ -4,6 +4,7 @@ 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 }