Skip to content

Commit 6b67f18

Browse files
authored
BugFix: Ensure callbacks only run once per instance. (#1712)
CallbacksObserver refactored to ignore an individual callback that has already been run on the given instance. EXAMPLE factory :user do name { "Jane" } trait :trait_a do trait_b end trait :trait_b do after(:build) { |user| user.name += " Doe" } end end user = build(:user, :trait_a, :trait_b) BEFORE: user.name #=> "Jane Doe Doe" AFTER: user.name #=> "Jane Doe"
1 parent 8e62514 commit 6b67f18

File tree

2 files changed

+37
-1
lines changed

2 files changed

+37
-1
lines changed

lib/factory_bot/callbacks_observer.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ class CallbacksObserver
44
def initialize(callbacks, evaluator)
55
@callbacks = callbacks
66
@evaluator = evaluator
7+
@completed = []
78
end
89

910
def update(name, result_instance)
1011
callbacks_by_name(name).each do |callback|
11-
callback.run(result_instance, @evaluator)
12+
if !completed?(result_instance, callback)
13+
callback.run(result_instance, @evaluator)
14+
record_completion!(result_instance, callback)
15+
end
1216
end
1317
end
1418

@@ -17,5 +21,19 @@ def update(name, result_instance)
1721
def callbacks_by_name(name)
1822
@callbacks.select { |callback| callback.name == name }
1923
end
24+
25+
def completed?(instance, callback)
26+
key = completion_key_for(instance, callback)
27+
@completed.include?(key)
28+
end
29+
30+
def record_completion!(instance, callback)
31+
key = completion_key_for(instance, callback)
32+
@completed << key
33+
end
34+
35+
def completion_key_for(instance, callback)
36+
"#{instance.object_id}-#{callback.object_id}"
37+
end
2038
end
2139
end

spec/acceptance/callbacks_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@
1616
after(:stub) { |user| user.last_name = "Double-Stubby" }
1717
after(:build) { |user| user.first_name = "Child-Buildy" }
1818
end
19+
20+
factory :user_with_multi_called_callbacks, class: :user do
21+
first_name { "Jane" }
22+
trait(:alias_2) { alias_1 }
23+
trait(:alias_1) { surname }
24+
trait :surname do
25+
after(:build) { |user| user.first_name += " Doe" }
26+
end
27+
end
1928
end
2029
end
2130

@@ -45,6 +54,15 @@
4554
user = FactoryBot.build(:user_with_inherited_callbacks)
4655
expect(user.first_name).to eq "Child-Buildy"
4756
end
57+
58+
it "only runs each callback once per instance" do
59+
user_1 = FactoryBot.build(:user_with_multi_called_callbacks, :surname, :alias_1, :alias_2)
60+
user_2 = FactoryBot.build(:user_with_multi_called_callbacks, :alias_1, :alias_2, :surname)
61+
user_3 = FactoryBot.build(:user_with_multi_called_callbacks, :alias_2, :surname, :alias_1)
62+
expect(user_1.first_name).to eq "Jane Doe"
63+
expect(user_2.first_name).to eq "Jane Doe"
64+
expect(user_3.first_name).to eq "Jane Doe"
65+
end
4866
end # with strategy callbacks
4967

5068
context "with before(:all) and after(:all) included" do

0 commit comments

Comments
 (0)