Introduce new callback syntax

Instead of calling before_create, after_build, after_create, or
after_stub, you can now call:

    before(:create) {|instance| instance.name = "overridden!" }
    after(:create)  {|instance| instance.name = "overridden!" }
    after(:build)   {|instance| instance.name = "overridden!" }
    after(:stub)    {|instance| instance.name = "overridden!" }

Additionally, you can declare callbacks longhand:

    callback(:after_stub) {|instance| instance.name = "overridden!" }

This allows for custom callbacks to be defined:

    callback(:custom_callback) {|instance| instance.name = "overridden!" }

Which can then be used from a custom strategy:

    class CustomStrategy
      def association(runner); end

      def result(evaluation)
        evaluation.object.tap do |instance|
          evaluation.notify(:custom_callback, instance)
        end
      end
    end

    FactoryGirl.register_strategy(:custom, CustomStrategy)

This would allow for calling:

    FactoryGirl.custom(:user)

Which would return the user instance but execute the :custom_callback callback
on the user instance first.
This commit is contained in:
Josh Clayton and Jason Draper 2012-05-04 16:48:46 -04:00 committed by Joshua Clayton
parent 95a4626daa
commit bef5a01b31
11 changed files with 182 additions and 42 deletions

View File

@ -207,7 +207,7 @@ factory :user do
name { "John Doe#{" - Rockstar" if rockstar}" } name { "John Doe#{" - Rockstar" if rockstar}" }
email { "#{name.downcase}@example.com" } email { "#{name.downcase}@example.com" }
after_create do |user, evaluator| after(:create) do |user, evaluator|
user.name.upcase! if evaluator.upcased user.name.upcase! if evaluator.upcased
end end
end end
@ -299,11 +299,11 @@ FactoryGirl.define do
posts_count 5 posts_count 5
end end
# the after_create yields two values; the user instance itself and the # the after(:create) yields two values; the user instance itself and the
# evaluator, which stores all values from the factory, including ignored # evaluator, which stores all values from the factory, including ignored
# attributes; `create_list`'s second argument is the number of records # attributes; `create_list`'s second argument is the number of records
# to create and we make sure the user is associated properly to the post # to create and we make sure the user is associated properly to the post
after_create do |user, evaluator| after(:create) do |user, evaluator|
FactoryGirl.create_list(:post, evaluator.posts_count, user: user) FactoryGirl.create_list(:post, evaluator.posts_count, user: user)
end end
end end
@ -589,18 +589,19 @@ FactoryGirl.create_list(:user, 3, :admin, :male, name: "Jon Snow")
Callbacks Callbacks
--------- ---------
factory\_girl makes available three callbacks for injecting some code: factory\_girl makes available four callbacks for injecting some code:
* after_build - called after a factory is built (via FactoryGirl.build) * after(:build) - called after a factory is built (via `FactoryGirl.build`, `FactoryGirl.create`)
* after_create - called after a factory is saved (via FactoryGirl.create) * before(:create) - called before a factory is saved (via `FactoryGirl.create`)
* after_stub - called after a factory is stubbed (via FactoryGirl.build_stubbed) * after(:create) - called after a factory is saved (via `FactoryGirl.create`)
* after(:stub) - called after a factory is stubbed (via `FactoryGirl.build_stubbed`)
Examples: Examples:
```ruby ```ruby
# Define a factory that calls the generate_hashed_password method after it is built # Define a factory that calls the generate_hashed_password method after it is built
factory :user do factory :user do
after_build { |user| generate_hashed_password(user) } after(:build) { |user| generate_hashed_password(user) }
end end
``` ```
@ -610,8 +611,8 @@ You can also define multiple types of callbacks on the same factory:
```ruby ```ruby
factory :user do factory :user do
after_build { |user| do_something_to(user) } after(:build) { |user| do_something_to(user) }
after_create { |user| do_something_else_to(user) } after(:create) { |user| do_something_else_to(user) }
end end
``` ```
@ -619,12 +620,12 @@ Factories can also define any number of the same kind of callback. These callba
```ruby ```ruby
factory :user do factory :user do
after_create { this_runs_first } after(:create) { this_runs_first }
after_create { then_this } after(:create) { then_this }
end end
``` ```
Calling FactoryGirl.create will invoke both after\_build and after\_create callbacks. Calling FactoryGirl.create will invoke both `after_build` and `after_create` callbacks.
Also, like standard attributes, child factories will inherit (and can also define) callbacks from their parent factory. Also, like standard attributes, child factories will inherit (and can also define) callbacks from their parent factory.
@ -677,7 +678,7 @@ When modifying a factory, you can change any of the attributes you want (aside f
`FactoryGirl.modify` must be called outside of a `FactoryGirl.define` block as it operates on factories differently. `FactoryGirl.modify` must be called outside of a `FactoryGirl.define` block as it operates on factories differently.
A caveat: you can only modify factories (not sequences or traits) and callbacks *still compound as they normally would*. So, if A caveat: you can only modify factories (not sequences or traits) and callbacks *still compound as they normally would*. So, if
the factory you're modifying defines an `after_create` callback, you defining an `after_create` won't override it, it'll just get run after the first callback. the factory you're modifying defines an `after(:create)` callback, you defining an `after(:create)` won't override it, it'll just get run after the first callback.
Building or Creating Multiple Records Building or Creating Multiple Records
------------------------------------- -------------------------------------
@ -810,6 +811,41 @@ FactoryGirl.json(:user)
Finally, you can override factory\_girl's own strategies if you'd like by Finally, you can override factory\_girl's own strategies if you'd like by
registering a new object in place of the strategies. registering a new object in place of the strategies.
Custom Callbacks
----------------
Custom callbacks can be defined if you're using custom strategies:
```ruby
class JsonStrategy
def initialize
@strategy = FactoryGirl.strategy_by_name(:create).new
end
delegate :association, to: :@strategy
def result(evaluation)
result = @strategy.result(evaluation)
evaluation.notify(:before_json, result)
result.to_json.tap do |json|
evaluation.notify(:after_json, json)
evaluation.notify(:make_json_awesome, json)
end
end
end
FactoryGirl.register_strategy(:json, JsonStrategy)
FactoryGirl.define do
factory :user do
before(:json) {|user| do_something_to(user) }
after(:json) {|user_json| do_something_to(user_json) }
callback(:make_json_awesome) {|user_json| do_something_to(user_json) }
end
end
```
Custom Methods to Persist Objects Custom Methods to Persist Objects
--------------------------------- ---------------------------------

View File

@ -1,3 +1,4 @@
require "set"
require "active_support/core_ext/module/delegation" require "active_support/core_ext/module/delegation"
require "active_support/notifications" require "active_support/notifications"
@ -103,9 +104,26 @@ module FactoryGirl
FactoryGirl.register_strategy(:null, FactoryGirl::Strategy::Null) FactoryGirl.register_strategy(:null, FactoryGirl::Strategy::Null)
end end
def self.callbacks
@callbacks ||= Set.new
end
def self.register_default_callbacks
register_callback(:after_build)
register_callback(:after_create)
register_callback(:after_stub)
register_callback(:before_create)
end
def self.callback_names def self.callback_names
[:after_build, :after_create, :after_stub, :before_create].freeze callbacks
end
def self.register_callback(name)
name = name.to_sym
callbacks << name
end end
end end
FactoryGirl.register_default_strategies FactoryGirl.register_default_strategies
FactoryGirl.register_default_callbacks

View File

@ -159,5 +159,18 @@ module FactoryGirl
def initialize_with(&block) def initialize_with(&block)
@definition.define_constructor(&block) @definition.define_constructor(&block)
end end
def before(name, &block)
callback("before_#{name}", &block)
end
def after(name, &block)
callback("after_#{name}", &block)
end
def callback(name, &block)
FactoryGirl.register_callback(name)
@definition.add_callback(Callback.new(name, block))
end
end end
end end

View File

@ -21,7 +21,7 @@ describe "using ActiveSupport::Instrumentation to track factory interaction" do
email "john@example.com" email "john@example.com"
factory :slow_user do factory :slow_user do
after_build { Kernel.sleep(0.1) } after(:build) { Kernel.sleep(0.1) }
end end
end end

View File

@ -6,35 +6,35 @@ describe "callbacks" do
FactoryGirl.define do FactoryGirl.define do
factory :user_with_callbacks, class: :user do factory :user_with_callbacks, class: :user do
after_stub { |user| user.first_name = 'Stubby' } after(:stub) { |user| user.first_name = 'Stubby' }
after_build { |user| user.first_name = 'Buildy' } after(:build) { |user| user.first_name = 'Buildy' }
after_create { |user| user.last_name = 'Createy' } after(:create) { |user| user.last_name = 'Createy' }
end end
factory :user_with_inherited_callbacks, parent: :user_with_callbacks do factory :user_with_inherited_callbacks, parent: :user_with_callbacks do
after_stub { |user| user.last_name = 'Double-Stubby' } after(:stub) { |user| user.last_name = 'Double-Stubby' }
after_build { |user| user.first_name = 'Child-Buildy' } after(:build) { |user| user.first_name = 'Child-Buildy' }
end end
end end
end end
it "runs the after_stub callback when stubbing" do it "runs the after(:stub) callback when stubbing" do
user = FactoryGirl.build_stubbed(:user_with_callbacks) user = FactoryGirl.build_stubbed(:user_with_callbacks)
user.first_name.should == 'Stubby' user.first_name.should == 'Stubby'
end end
it "runs the after_build callback when building" do it "runs the after(:build) callback when building" do
user = FactoryGirl.build(:user_with_callbacks) user = FactoryGirl.build(:user_with_callbacks)
user.first_name.should == 'Buildy' user.first_name.should == 'Buildy'
end end
it "runs both the after_build and after_create callbacks when creating" do it "runs both the after(:build) and after(:create) callbacks when creating" do
user = FactoryGirl.create(:user_with_callbacks) user = FactoryGirl.create(:user_with_callbacks)
user.first_name.should == 'Buildy' user.first_name.should == 'Buildy'
user.last_name.should == 'Createy' user.last_name.should == 'Createy'
end end
it "runs both the after_stub callback on the factory and the inherited after_stub callback" do it "runs both the after(:stub) callback on the factory and the inherited after(:stub) callback" do
user = FactoryGirl.build_stubbed(:user_with_inherited_callbacks) user = FactoryGirl.build_stubbed(:user_with_inherited_callbacks)
user.first_name.should == 'Stubby' user.first_name.should == 'Stubby'
user.last_name.should == 'Double-Stubby' user.last_name.should == 'Double-Stubby'
@ -56,9 +56,9 @@ describe "callbacks using syntax methods without referencing FactoryGirl explici
sequence(:sequence_3) sequence(:sequence_3)
factory :user do factory :user do
after_stub { generate(:sequence_3) } after(:stub) { generate(:sequence_3) }
after_build {|user| user.first_name = generate(:sequence_1) } after(:build) {|user| user.first_name = generate(:sequence_1) }
after_create {|user, evaluator| user.last_name = generate(:sequence_2) } after(:create) {|user, evaluator| user.last_name = generate(:sequence_2) }
end end
end end
end end
@ -76,3 +76,76 @@ describe "callbacks using syntax methods without referencing FactoryGirl explici
FactoryGirl.create(:user).last_name.should == 1 FactoryGirl.create(:user).last_name.should == 1
end end
end end
describe "custom callbacks" do
let(:custom_before) do
Class.new do
def result(evaluation)
evaluation.object.tap do |instance|
evaluation.notify(:before_custom, instance)
end
end
end
end
let(:custom_after) do
Class.new do
def result(evaluation)
evaluation.object.tap do |instance|
evaluation.notify(:after_custom, instance)
end
end
end
end
let(:totally_custom) do
Class.new do
def result(evaluation)
evaluation.object.tap do |instance|
evaluation.notify(:totally_custom, instance)
end
end
end
end
before do
define_model("User", first_name: :string, last_name: :string) do
def name
[first_name, last_name].join(" ")
end
end
FactoryGirl.register_strategy(:custom_before, custom_before)
FactoryGirl.register_strategy(:custom_after, custom_after)
FactoryGirl.register_strategy(:totally_custom, totally_custom)
FactoryGirl.define do
factory :user do
first_name "John"
last_name "Doe"
before(:custom) {|instance| instance.first_name = "Overridden First" }
after(:custom) {|instance| instance.last_name = "Overridden Last" }
callback(:totally_custom) do |instance|
instance.first_name = "Totally"
instance.last_name = "Custom"
end
end
end
end
it "runs a custom before callback when the proper strategy executes" do
FactoryGirl.build(:user).name.should == "John Doe"
FactoryGirl.custom_before(:user).name.should == "Overridden First Doe"
end
it "runs a custom after callback when the proper strategy executes" do
FactoryGirl.build(:user).name.should == "John Doe"
FactoryGirl.custom_after(:user).name.should == "John Overridden Last"
end
it "runs a custom callback without prepending before or after when the proper strategy executes" do
FactoryGirl.build(:user).name.should == "John Doe"
FactoryGirl.totally_custom(:user).name.should == "Totally Custom"
end
end

View File

@ -64,7 +64,7 @@ describe "multiple creates and ignored attributes to dynamically build attribute
posts_count 5 posts_count 5
end end
after_create do |user, evaluator| after(:create) do |user, evaluator|
FactoryGirl.create_list(:post, evaluator.posts_count, user: user) FactoryGirl.create_list(:post, evaluator.posts_count, user: user)
end end
end end

View File

@ -12,7 +12,7 @@ describe "modifying factories" do
factory :user do factory :user do
email email
after_create do |user| after(:create) do |user|
user.login = user.name.upcase if user.name user.login = user.name.upcase if user.name
end end
@ -58,7 +58,7 @@ describe "modifying factories" do
FactoryGirl.modify do FactoryGirl.modify do
factory :user do factory :user do
name "Great User" name "Great User"
after_create do |user| after(:create) do |user|
user.name = user.name.downcase user.name = user.name.downcase
user.login = nil user.login = nil
end end

View File

@ -78,7 +78,7 @@ describe "nested factories with different parents" do
end end
factory :uppercase_male_user, parent: :male_user do factory :uppercase_male_user, parent: :male_user do
after_build {|user| user.name = user.name.upcase } after(:build) {|user| user.name = user.name.upcase }
end end
end end
end end

View File

@ -187,11 +187,11 @@ describe "traits with callbacks" do
name "John" name "John"
trait :great do trait :great do
after_create {|user| user.name.upcase! } after(:create) {|user| user.name.upcase! }
end end
trait :awesome do trait :awesome do
after_create {|user| user.name = "awesome" } after(:create) {|user| user.name = "awesome" }
end end
factory :caps_user, traits: [:great] factory :caps_user, traits: [:great]
@ -232,7 +232,7 @@ describe "traits added via strategy" do
end end
trait :great do trait :great do
after_create {|user| user.name.upcase! } after(:create) {|user| user.name.upcase! }
end end
end end
end end

View File

@ -17,7 +17,7 @@ describe "transient attributes" do
name { "#{FactoryGirl.generate(:name)}#{" - Rockstar" if rockstar}" } name { "#{FactoryGirl.generate(:name)}#{" - Rockstar" if rockstar}" }
email { "#{name.downcase}#{four}@example.com" } email { "#{name.downcase}#{four}@example.com" }
after_create do |user, evaluator| after(:create) do |user, evaluator|
user.name.upcase! if evaluator.upcased user.name.upcase! if evaluator.upcased
end end
end end

View File

@ -126,18 +126,18 @@ describe FactoryGirl::DefinitionProxy, "adding callbacks" do
let(:proxy) { FactoryGirl::DefinitionProxy.new(subject) } let(:proxy) { FactoryGirl::DefinitionProxy.new(subject) }
let(:callback) { -> { "my awesome callback!" } } let(:callback) { -> { "my awesome callback!" } }
context "#after_build" do context "#after(:build)" do
before { proxy.after_build(&callback) } before { proxy.after(:build, &callback) }
it { should have_callback(:after_build).with_block(callback) } it { should have_callback(:after_build).with_block(callback) }
end end
context "#after_create" do context "#after(:create)" do
before { proxy.after_create(&callback) } before { proxy.after(:create, &callback) }
it { should have_callback(:after_create).with_block(callback) } it { should have_callback(:after_create).with_block(callback) }
end end
context "#after_stub" do context "#after(:stub)" do
before { proxy.after_stub(&callback) } before { proxy.after(:stub, &callback) }
it { should have_callback(:after_stub).with_block(callback) } it { should have_callback(:after_stub).with_block(callback) }
end end
end end