diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index d2924c4..65fff81 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -745,6 +745,49 @@ You can override the initializer in order to: * Use a method other than `new` to instantiate the instance * Do crazy things like decorate the instance after it's built +Custom Strategies +----------------- + +There are times where you may want to extend behavior of factory\_girl by +adding a custom build strategy. + +Strategies define two methods: `association` and `result`. `association` +receives a `FactoryGirl::FactoryRunner` instance, upon which you can call +`run`, overriding the strategy if you want. The second method, `result`, +receives a `FactoryGirl::Evaluation` instance. It provides a way to trigger +callbacks (with `notify`), `object` or `hash` (to get the result instance or a +hash based on the attributes defined in the factory), and `create`, which +executes the `to_create` callback defined on the factory. + +To understand how factory\_girl uses strategies internally, it's probably +easiest to just view the source for each of the four default strategies. + +Inheritance can occasionally be useful; here's an example of inheriting from +`FactoryGirl::Strategy::Create` to build a JSON representation of your model. + +```ruby +class JsonStrategy < FactoryGirl::Strategy::Create + def result(evaluation) + super.to_json + end +end +``` + +For factory\_girl to recognize the new strategy, you can register it: + +```ruby +FactoryGirl.register_strategy(:json, JsonStrategy) +``` + +This allows you to call + +```ruby +FactoryGirl.json(:user) +``` + +Finally, you can override factory\_girl's own strategies if you'd like by +registering a new object in place of the strategies. + Cucumber Integration -------------------- diff --git a/lib/factory_girl.rb b/lib/factory_girl.rb index 2f2cf26..162c20f 100644 --- a/lib/factory_girl.rb +++ b/lib/factory_girl.rb @@ -80,7 +80,34 @@ module FactoryGirl traits.find(name) end + def self.strategies + @strategies ||= Registry.new("Strategy") + end + + def self.register_strategy(strategy_name, strategy_class) + strategies.register(strategy_name, strategy_class) + + FactoryGirl::Syntax::Methods.module_exec do + define_method(strategy_name) do |name, *traits_and_overrides, &block| + FactoryRunner.new(name, strategy_class, traits_and_overrides).run(&block) + end + end + end + + def self.strategy_by_name(name) + strategies.find(name) + end + + def self.register_default_strategies + FactoryGirl.register_strategy(:build, FactoryGirl::Strategy::Build) + FactoryGirl.register_strategy(:create, FactoryGirl::Strategy::Create) + FactoryGirl.register_strategy(:attributes_for, FactoryGirl::Strategy::AttributesFor) + FactoryGirl.register_strategy(:build_stubbed, FactoryGirl::Strategy::Stub) + end + def self.callback_names [:after_build, :after_create, :after_stub, :before_create].freeze end end + +FactoryGirl.register_default_strategies diff --git a/lib/factory_girl/evaluator.rb b/lib/factory_girl/evaluator.rb index 91d5567..6d79d01 100644 --- a/lib/factory_girl/evaluator.rb +++ b/lib/factory_girl/evaluator.rb @@ -28,7 +28,7 @@ module FactoryGirl end def association(factory_name, overrides = {}) - strategy_override = overrides.fetch(:strategy) { Strategy::Create } + strategy_override = overrides.fetch(:strategy) { FactoryGirl.strategy_by_name(:create) } build_strategy = StrategyCalculator.new(strategy_override).strategy runner = FactoryRunner.new(factory_name, build_strategy, [overrides.except(:strategy)]) diff --git a/lib/factory_girl/reload.rb b/lib/factory_girl/reload.rb index 5608311..35244a1 100644 --- a/lib/factory_girl/reload.rb +++ b/lib/factory_girl/reload.rb @@ -3,6 +3,8 @@ module FactoryGirl self.factories.clear self.sequences.clear self.traits.clear + self.strategies.clear + self.register_default_strategies self.find_definitions end end diff --git a/lib/factory_girl/strategy_calculator.rb b/lib/factory_girl/strategy_calculator.rb index 86ab371..2491491 100644 --- a/lib/factory_girl/strategy_calculator.rb +++ b/lib/factory_girl/strategy_calculator.rb @@ -19,11 +19,8 @@ module FactoryGirl end def strategy_name_to_object - case @name_or_object - when :build then Strategy::Build - when :create then Strategy::Create - else raise "unrecognized method #{@name_or_object}" - end + FactoryGirl.strategy_by_name(@name_or_object) || + raise("unrecognized method #{@name_or_object}") end end end diff --git a/lib/factory_girl/syntax/generate.rb b/lib/factory_girl/syntax/generate.rb index aa55e8e..f4e36cc 100644 --- a/lib/factory_girl/syntax/generate.rb +++ b/lib/factory_girl/syntax/generate.rb @@ -43,7 +43,7 @@ module FactoryGirl def generate(overrides = {}, &block) ActiveSupport::Deprecation.warn "Model.generate is deprecated; use the FactoryGirl.define syntax instead", caller - instance = FactoryRunner.new(name.underscore, Strategy::Build, [overrides]).run + instance = FactoryRunner.new(name.underscore, FactoryGirl.strategy_by_name(:build), [overrides]).run instance.save yield(instance) if block_given? instance @@ -51,14 +51,14 @@ module FactoryGirl def generate!(overrides = {}, &block) ActiveSupport::Deprecation.warn "Model.generate! is deprecated; use the FactoryGirl.define syntax instead", caller - instance = FactoryRunner.new(name.underscore, Strategy::Create, [overrides]).run + instance = FactoryRunner.new(name.underscore, FactoryGirl.strategy_by_name(:create), [overrides]).run yield(instance) if block_given? instance end def spawn(overrides = {}, &block) ActiveSupport::Deprecation.warn "Model.spawn is deprecated; use the FactoryGirl.define syntax instead", caller - instance = FactoryRunner.new(name.underscore, Strategy::Build, [overrides]).run + instance = FactoryRunner.new(name.underscore, FactoryGirl.strategy_by_name(:build), [overrides]).run yield(instance) if block_given? instance end diff --git a/lib/factory_girl/syntax/make.rb b/lib/factory_girl/syntax/make.rb index c7ea97e..c28c6e1 100644 --- a/lib/factory_girl/syntax/make.rb +++ b/lib/factory_girl/syntax/make.rb @@ -29,12 +29,12 @@ module FactoryGirl def make(overrides = {}) ActiveSupport::Deprecation.warn "Model.make is deprecated; use the FactoryGirl.define syntax instead", caller - FactoryRunner.new(name.underscore, Strategy::Build, [overrides]).run + FactoryRunner.new(name.underscore, FactoryGirl.strategy_by_name(:build), [overrides]).run end def make!(overrides = {}) ActiveSupport::Deprecation.warn "Model.make! is deprecated; use the FactoryGirl.define syntax instead", caller - FactoryRunner.new(name.underscore, Strategy::Create, [overrides]).run + FactoryRunner.new(name.underscore, FactoryGirl.strategy_by_name(:create), [overrides]).run end end diff --git a/lib/factory_girl/syntax/methods.rb b/lib/factory_girl/syntax/methods.rb index 814faf6..23092da 100644 --- a/lib/factory_girl/syntax/methods.rb +++ b/lib/factory_girl/syntax/methods.rb @@ -1,87 +1,6 @@ module FactoryGirl module Syntax module Methods - # Generates and returns a Hash of attributes from this factory. Attributes - # can be individually overridden by passing in a Hash of attribute => value - # pairs. - # - # Arguments: - # * name: +Symbol+ or +String+ - # The name of the factory that should be used. - # * traits_and_overrides: +Array+ - # [+*Array+] Traits to be applied - # [+Hash+] Attributes to overwrite for this set. - # * block: - # Yields the hash of attributes. - # - # Returns: +Hash+ - # A set of attributes that can be used to build an instance of the class - # this factory generates. - def attributes_for(name, *traits_and_overrides, &block) - FactoryRunner.new(name, Strategy::AttributesFor, traits_and_overrides).run(&block) - end - - # Generates and returns an instance from this factory. Attributes can be - # individually overridden by passing in a Hash of attribute => value pairs. - # - # Arguments: - # * name: +Symbol+ or +String+ - # The name of the factory that should be used. - # * traits_and_overrides: +Array+ - # [+*Array+] Traits to be applied - # [+Hash+] Attributes to overwrite for this instance. - # * block: - # Yields the built instance. - # - # Returns: +Object+ - # An instance of the class this factory generates, with generated attributes - # assigned. - def build(name, *traits_and_overrides, &block) - FactoryRunner.new(name, Strategy::Build, traits_and_overrides).run(&block) - end - - # Generates, saves, and returns an instance from this factory. Attributes can - # be individually overridden by passing in a Hash of attribute => value - # pairs. - # - # Instances are saved using the +save!+ method, so ActiveRecord models will - # raise ActiveRecord::RecordInvalid exceptions for invalid attribute sets. - # - # Arguments: - # * name: +Symbol+ or +String+ - # The name of the factory that should be used. - # * traits_and_overrides: +Array+ - # [+*Array+] Traits to be applied - # [+Hash+] Attributes to overwrite for this instance. - # * block: - # Yields the created instance. - # - # Returns: +Object+ - # A saved instance of the class this factory generates, with generated - # attributes assigned. - def create(name, *traits_and_overrides, &block) - FactoryRunner.new(name, Strategy::Create, traits_and_overrides).run(&block) - end - - # Generates and returns an object with all attributes from this factory - # stubbed out. Attributes can be individually overridden by passing in a Hash - # of attribute => value pairs. - # - # Arguments: - # * name: +Symbol+ or +String+ - # The name of the factory that should be used. - # * traits_and_overrides: +Array+ - # [+*Array+] Traits to be applied - # [+Hash+] Attributes to overwrite for this instance. - # * block - # Yields the stubbed object. - # - # Returns: +Object+ - # An object with generated attributes stubbed out. - def build_stubbed(name, *traits_and_overrides, &block) - FactoryRunner.new(name, Strategy::Stub, traits_and_overrides).run(&block) - end - # Builds and returns multiple instances from this factory as an array. Attributes can be # individually overridden by passing in a Hash of attribute => value pairs. # diff --git a/spec/acceptance/register_strategies_spec.rb b/spec/acceptance/register_strategies_spec.rb new file mode 100644 index 0000000..8cf2322 --- /dev/null +++ b/spec/acceptance/register_strategies_spec.rb @@ -0,0 +1,120 @@ +require "spec_helper" + +shared_context "registering custom strategies" do + before do + define_class("NamedObject") do + attr_accessor :name + end + end + + let(:custom_strategy) do + Class.new do + def result(evaluation) + evaluation.object.tap do |instance| + instance.name = "Custom strategy" + end + end + end + end +end + +describe "register custom strategies" do + include_context "registering custom strategies" + + before do + FactoryGirl.define do + factory :named_object do + name "Great" + end + end + end + + it "allows overriding default strategies" do + FactoryGirl.build(:named_object).name.should == "Great" + FactoryGirl.register_strategy(:build, custom_strategy) + FactoryGirl.build(:named_object).name.should == "Custom strategy" + end + + it "allows adding additional strategies" do + FactoryGirl.register_strategy(:insert, custom_strategy) + + FactoryGirl.build(:named_object).name.should == "Great" + FactoryGirl.insert(:named_object).name.should == "Custom strategy" + end +end + +describe "including FactoryGirl::Syntax::Methods when custom strategies have been declared" do + include FactoryGirl::Syntax::Methods + + include_context "registering custom strategies" + + before do + FactoryGirl.define do + factory :named_object do + name "Great" + end + end + end + + it "allows adding additional strategies" do + FactoryGirl.register_strategy(:insert, custom_strategy) + + insert(:named_object).name.should == "Custom strategy" + end +end + +describe "associations without overriding :strategy" do + include_context "registering custom strategies" + + before do + define_model("Post", user_id: :integer) do + belongs_to :user + end + + define_model("User", name: :string) + + FactoryGirl.define do + factory :post do + user + end + + factory :user do + name "John Doe" + end + end + end + + it "uses the overridden create strategy to create the association" do + FactoryGirl.register_strategy(:create, custom_strategy) + post = FactoryGirl.build(:post) + post.user.name.should == "Custom strategy" + end +end + +describe "associations overriding :strategy" do + include_context "registering custom strategies" + + before do + define_model("Post", user_id: :integer) do + belongs_to :user + end + + define_model("User", name: :string) + + FactoryGirl.define do + factory :post do + association :user, strategy: :insert + end + + factory :user do + name "John Doe" + end + end + end + + it "uses the overridden create strategy to create the association" do + FactoryGirl.register_strategy(:insert, custom_strategy) + post = FactoryGirl.build(:post) + post.user.name.should == "Custom strategy" + end +end