Rails Callback Debugging
Throughout my career as a Rails developer there have been a number of times that using Rails callbacks for debugging purposes has come in handy and saved me a ton of time and frustration, so I thought I’d take some time to share this approach. To put it simply, if you’re seeing any odd behavior relating to a Rails object being created, updated, or destroyed, this may be a way for you to easily get to the bottom of what is going on. Some might argue this approach is a bit hacky, but when it comes to debugging I think anything goes as long as it gives you the right answers.
When To Use Callbacks for Debugging
While this technique could be used for any of the Rails callbacks, I have mostly used it in the following contexts.
- A Rails object is unexpectedly being created (use
after_create
for debugging) - A Rails object is unexpectedly being updated (use
after_update
for debugging) - A Rails object is unexpectedly being destroyed (use
after_destroy
for debugging)
How To Use Callbacks for Debugging
The implementation of this is relatively simple; if one the aforementioned unexpected behaviors is occurring, put the related callback in the correct model with a binding.pry
(see pry) and use caller
(see caller) to help identify the source of the unexpected behavior. Let’s look at a simple example.
Models
class Team < ApplicationRecord
has_many :players
def create_player(name:)
players.create(name: name)
end
end
class Player < ApplicationRecord
belongs_to :team
end
Factory
FactoryBot.define do
factory :team do
trait :bucks do
name { 'Bucks' }
after(:build) do |team|
team.create_player(name: 'Giannis Antetokounmpo')
end
end
end
end
Test
describe Team do
let!(:bucks) { create(:team, :bucks) }
describe '#create_player' do
let(:team) { create(:team) }
it 'creates a player' do
team.create_player(name: 'John')
expect(Player.count).to eq(1)
end
end
end
This test is going to fail because the player count will be 2 and not 1. The reason for this is that let!(:team) { create(:team, :bucks) }
(specifically the :bucks
attribute) causes the Team
factory to create the Giannis Antetokounmpo
player for that team, so when team.create_player(name: 'John')
is called there is already an existing player. See factory_bot for more info. While this is relatively easy to spot in this simple example, in a real world situation the let!(:team) { create(:team, :bucks) }
could be hundreds of lines above the failing test, and getting to the bottom of why this test is failing could take a long time. This is where using callbacks comes into play, so let’s add that now.
class Player < ApplicationRecord
belongs_to :team
after_create :debug_test_failure // Callback added for debugging
def debug_test_failure
binding.pry
end
end
When I run the test again with this debug_test_failure
callback I will hit the binding.pry
every time a player is created. Once this pry is hit I can call caller
to see the execution stack, which will point me directly to code that is creating the player. For this test my binding.pry
will be hit twice (once for let!(:team){ create(:team, :bucks) }
and once for team.create_player(name: 'John’)
), and it’s on the first hit that I will be pointed to the line of code that is the culprit. From there I can fix the test accordingly and then remove the debugging callback I added to the player model.
It is worth noting that the simple example above, while helpful for capturing the technique of using callbacks for debugging, does not fully illustrate the full potential of this technique. It’s when you’re dealing with code bases that have evolved over years and have many dusty corners that this can really save you a ton of time and frustration. So, hopefully the next time you are scratching your head over the source of some unexpected Rails object behavior, you can apply this approach.