Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -2084,6 +2084,26 @@ config.after(:suite) do
end
```

You can also subscribe to `factory_bot.before_run_factory` to be notified
before a factory is run. This is useful for building a factory call stack when
debugging deeply nested associations:

```ruby
factory_call_stack = []

ActiveSupport::Notifications.subscribe("factory_bot.before_run_factory") do |name, start, finish, id, payload|
factory_call_stack.push(payload[:name])
end

ActiveSupport::Notifications.subscribe("factory_bot.run_factory") do |name, start, finish, id, payload|
factory_call_stack.pop
end
```

The payload for `factory_bot.before_run_factory` contains the same keys as
`factory_bot.run_factory`: `:name`, `:strategy`, `:traits`, `:overrides`, and
`:factory`.

Another example could involve tracking the attributes and traits that factories are compiled with. If you're using RSpec, you could add `before(:suite)` and `after(:suite)` blocks that subscribe to `factory_bot.compile_factory` notifications:

```ruby
Expand Down
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# News

## Unreleased

* Feat: Added `factory_bot.before_run_factory` instrumentation event

## 6.5.6 (October 22, 2025)

* Fix: Enforce association override precedence over trait foreign keys by @JinOketani in [#1768](https://github.com/thoughtbot/factory_bot/pull/1768)
Expand Down
20 changes: 20 additions & 0 deletions docs/src/activesupport-instrumentation/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ config.after(:suite) do
end
```

You can also subscribe to `factory_bot.before_run_factory` to be notified
before a factory is run. This is useful for building a factory call stack when
debugging deeply nested associations:

```ruby
factory_call_stack = []

ActiveSupport::Notifications.subscribe("factory_bot.before_run_factory") do |name, start, finish, id, payload|
factory_call_stack.push(payload[:name])
end

ActiveSupport::Notifications.subscribe("factory_bot.run_factory") do |name, start, finish, id, payload|
factory_call_stack.pop
end
```

The payload for `factory_bot.before_run_factory` contains the same keys as
`factory_bot.run_factory`: `:name`, `:strategy`, `:traits`, `:overrides`, and
`:factory`.

Another example could involve tracking the attributes and traits that factories are compiled with. If you're using RSpec, you could add `before(:suite)` and `after(:suite)` blocks that subscribe to `factory_bot.compile_factory` notifications:

```ruby
Expand Down
2 changes: 2 additions & 0 deletions lib/factory_bot/factory_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ def run(runner_strategy = @strategy, &block)
factory: factory
}

ActiveSupport::Notifications.instrument("factory_bot.before_run_factory", instrumentation_payload)

ActiveSupport::Notifications.instrument("factory_bot.run_factory", instrumentation_payload) do
factory.run(runner_strategy, @overrides, &block)
end
Expand Down
87 changes: 87 additions & 0 deletions spec/acceptance/activesupport_instrumentation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,93 @@ def subscribed(callback, *args)
end
end

describe "using ActiveSupport::Instrumentation to track before_run_factory interaction" do
before do
define_model("User", email: :string)
define_model("Post", user_id: :integer) do
belongs_to :user
end

FactoryBot.define do
factory :user do
email { "john@example.com" }
end

factory :post do
trait :with_user do
user
end
end
end
end

it "builds the correct payload" do
tracked_payloads = []
callback = ->(_name, _start, _finish, _id, payload) { tracked_payloads << payload }

ActiveSupport::Notifications.subscribed(callback, "factory_bot.before_run_factory") do
FactoryBot.build(:user)
FactoryBot.create(:post, :with_user)
end

user_payload = tracked_payloads.detect { |p| p[:name] == :user }
expect(user_payload[:strategy]).to eq(:build)
expect(user_payload[:traits]).to eq([])
expect(user_payload[:overrides]).to eq({})
expect(user_payload[:factory]).to be_a(FactoryBot::Factory)

post_payload = tracked_payloads.detect { |p| p[:name] == :post }
expect(post_payload[:strategy]).to eq(:create)
expect(post_payload[:traits]).to eq([:with_user])
expect(post_payload[:factory]).to be_a(FactoryBot::Factory)
end

it "fires before run_factory completes" do
events = []

before_callback = ->(_name, _start, _finish, _id, payload) {
events << [:before, payload[:name]]
}
run_callback = ->(_name, _start, _finish, _id, payload) {
events << [:run, payload[:name]]
}

ActiveSupport::Notifications.subscribed(before_callback, "factory_bot.before_run_factory") do
ActiveSupport::Notifications.subscribed(run_callback, "factory_bot.run_factory") do
FactoryBot.build(:user)
end
end

expect(events).to eq([[:before, :user], [:run, :user]])
end

it "captures nested factory call stack" do
call_stack = []

before_callback = ->(_name, _start, _finish, _id, payload) {
call_stack.push(payload[:name])
}
run_callback = ->(_name, _start, _finish, _id, payload) {
call_stack.pop
}

stack_during_user_build = nil
user_before = ->(_name, _start, _finish, _id, payload) {
stack_during_user_build = call_stack.dup if payload[:name] == :user
}

ActiveSupport::Notifications.subscribed(before_callback, "factory_bot.before_run_factory") do
ActiveSupport::Notifications.subscribed(run_callback, "factory_bot.run_factory") do
ActiveSupport::Notifications.subscribed(user_before, "factory_bot.before_run_factory") do
FactoryBot.create(:post, :with_user)
end
end
end

expect(stack_during_user_build).to eq([:post, :user])
end
end

describe "using ActiveSupport::Instrumentation to track compile_factory interaction" do
before do
define_model("User", name: :string, email: :string)
Expand Down