Add ability to register custom strategies

This commit is contained in:
Joshua Clayton 2012-04-20 17:22:37 -04:00
parent c006dd186f
commit c8c1a801ed
9 changed files with 200 additions and 92 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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