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
This commit is contained in:
Joshua Clayton 2012-01-20 13:04:48 -05:00
parent 5555f14db7
commit 578036480f
9 changed files with 220 additions and 7 deletions

View File

@ -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')
```
```

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }