Skip to content

Commit

Permalink
Implement initialize_with to allow overriding object instantiation
Browse files Browse the repository at this point in the history
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
  • Loading branch information
joshuaclayton committed Jan 20, 2012
1 parent 5555f14 commit 5780364
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 7 deletions.
38 changes: 37 additions & 1 deletion GETTING_STARTED.md
Expand Up @@ -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" }

This comment has been minimized.

Copy link
@r00k

r00k Jan 21, 2012

Contributor

Doesn't look like a name sequence.


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.

This comment has been minimized.

Copy link
@r00k

r00k Jan 21, 2012

Contributor

I'm not the smartest guy ever, but I had trouble understanding this example and had to read the tests to see what's going on.

I think part of the problem is that you're still initializing with .new; it'd be clearer with a different method.

Also, maybe you could include a line or two about why you'd want this. I'm having trouble picturing when I'd use this.

This comment has been minimized.

Copy link
@jferris

jferris Jan 21, 2012

Member

The primary reason you'd need this is if some/all attributes are passed to the constructor.

factory_girl was originally written with ActiveRecord in mind. Because of this, it initializes all instances by calling new on your build class without any arguments. It then calls attribute writer methods to assign all the attribute values. While that works fine for ActiveRecord, it actually doesn't work for almost any pure Ruby class.

This change lets you:

  • Build non-ActiveRecord objects that accept arguments to the constructor
  • Use a different method to instantiate the instant
  • Do crazy things like decorate the instance after it's built

This comment has been minimized.

Copy link
@r00k

r00k Jan 21, 2012

Contributor

Super-helpful explanation. I get it now.

I think this more-lengthy explanation (or most of it) belongs in the README. I doubt I'm the only one who will be confused by it as written.

P.S. Is this sort of feedback helpful? Or is this more of a "well then update it yourself, jerk" kind of situation?

This comment has been minimized.

Copy link
@jferris

jferris via email Jan 23, 2012

Member

This comment has been minimized.

Copy link
@burningTyger

burningTyger Jan 23, 2012

Let me add that I also appreciate this clarification.

Cucumber Integration
--------------------

Expand Down Expand Up @@ -620,4 +656,4 @@ User.blueprint do
end

User.make(:name => 'Johnny')
```
```
6 changes: 3 additions & 3 deletions 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 = []
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion 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)
Expand All @@ -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
Expand Down Expand Up @@ -50,6 +51,10 @@ def define_trait(trait)
@defined_traits << trait
end

def define_constructor(&block)
@constructor = block
end

private

def base_traits
Expand Down
4 changes: 4 additions & 0 deletions lib/factory_girl/definition_proxy.rb
Expand Up @@ -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
11 changes: 10 additions & 1 deletion lib/factory_girl/factory.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/factory_girl/null_factory.rb
Expand Up @@ -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
Expand Down
147 changes: 147 additions & 0 deletions 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
11 changes: 11 additions & 0 deletions spec/factory_girl/definition_proxy_spec.rb
Expand Up @@ -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
1 change: 1 addition & 0 deletions spec/factory_girl/null_factory_spec.rb
Expand Up @@ -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 }
Expand Down

0 comments on commit 5780364

Please sign in to comment.