How I debug with Ruby
Over the many years that I have been working with Ruby, I have never really learned to use a debugger, and really only lean on tools like Pry to start a REPL session at a particular line in some Ruby code. From there, I reach for Ruby's built in debugging tools, and thus far, they have served me well! Below is a non-exhaustive list of some of the common techniques I use to work out what the heck is going on with some piece of code.
How do I work out what methods are available on a class?
The methods
method lets you see which methods an class defines. This is useful when working with an un-documented or otherwise unknown class, but have an idea of what you might want to achieve. The return value of this call is an array of symbols that represent method names.
Foo.methods #=> [:bar, :baz, ...]
If you run this yourself on a class, you'll see a huge list of method names, including things like :new, :==, :inspect
. This is is because method
also returns a list of methods that the ancestors of your object respond to. If you want to clean up this you can:
Pass the false
argument in to methods
:
Foo.methods(false) => [:bar, :baz]
Or selectively remove the methods of known ancestors from the resulting output:
Foo.methods - Object.methods => [:bar, :baz]
There is also the instance_methods
method that does the same thing, but only returns instance methods.
The following calls are almost the same:
Foo.new.methods #=> includes methods that belong to ancestors
Foo.instance_methods(false)
Ok cool, now we have an idea of the methods, but what about actually calling them?
The parameters
method lets you take a peak at the arguments required to call a method. This is not always bulletproof, as some methods take a variable number of args (e.g. def foo(*args)
)
Anyway, once we have a method to inspect, we can use parameters
like so:
class Foo
def bar(one:, two:)
end
def baz(one, two = nil, three: nil)
end
end
Foo.instance_method(:bar).parameters #=> [[:keyreq, :one], [:keyreq, :two]]
Foo.instance_method(:baz) #=> [[:req, :one], [:opt, :two], [:key, :three]]
You can even see if a method has a default provided (:key
, :opt
), or is required (:req
, :keyreq
) along with its name.
Workout where a method is defined.
Sometimes, you're calling a method and the behaviour being presented is not at all what you expect, or even worse, your myriad of puts
and raise
calls you've put in place to debug the behaviour are not being called!
Here's an example:
# source/foo.rb
class Foo
def bar(arg)
raise "here".inspect
# do the method here
end
end
Foo.new.bar(arg) #=> Nothing is raised π±
Inspecting the bar
method with source_location
should help work out what is going on here:
Foo.instance_method(:bar).source_location #=> ['some_monkey_patch.rb']
In our case here, we can see that something else is being called instead of our method in our class! While this exact example is not super common, I have been bitten by this a couple of times by some gems implementing a to_h
, to_json
, to_s
etc. method on basic Ruby classes that then cause unexpected behaviour.
Another variation of this is when you have a super-class that is calling super
in its initialiser (or some other place) but you don't know where that goes.
class Foo < Bar
def call
puts method(:call).super_method.source_location #=> ['/gems/path/bar.rb#42']
# super
end
end
You can then use something like cat
in the terminal to inspect the file and get a sense of what Bar's super method will call.
Or better than cat
, you can use bundle open
to dig around a gem with your editor of choice (discussed in the next section).
Inspecting a Gem
Sometimes, the code you're having trouble with is in a gem! There are a couple of ways to go about inspecting this code. If the code is hosted on Github, then that's a fairly simple way of spelunking through code, with the benefit of having commit messages right there that could explain the behaviour you're seeing (just make sure you're looking at the right version).
But, you can also use bundle open gemname
to have bundler open the correct version of the gem in your EDITOR
of choice. From here, you can throw in debugging statements, make changes, and even fix a bug or two if you find any :)
If you're spelunking has lead you to drop puts
statements through many files, you can clean the gem up quickly with gem pristine gemname
(or gem pristine --all
if your debugging journey spanned multiple gems!).
How did we get here?
We've discussed working out where a method call will take us, but what about how we got to a particular method.
For this, we can use caller
. caller
will print out a massive stack trace showing you how we got here, but the first line is the one that really matters:
class Foo
def bar
caller
end
end
irb(main):072:0> Foo.new.bar #=> ["(irb):72:in `<top (required)>'","/Users/danielnitsikopoulos/.asdf/installs/ruby/3.2.0/lib/ruby/3.2.0/irb/workspace.rb:119:in `eval'", ...]
in this case, I'm running the code in irb, so (irb):72
is where this is being called from.
Formatting
The last thing I want to touch on, is how to do some basic formatting to make inspecting output a little easier.
Add some markers to split puts
calls, especially when calling something in a loop:
def foo
puts "Doing something:"
do_something
puts "*" * 88 # print out 88 stars
end
2.times { foo } #=>
Doing something:
****************************************************************************************
Doing something:
****************************************************************************************
When looking through a collection of data to understand something about it, also print out things like IDs so that you can pick out the one you want:
some_collection.map {|c| [c.id, c.name] } #=>
[
[123, "foo"],
[234, "bar"],
[345, "baz"],
...
]
Conclusion
Ruby's built in meta programming tools are super powerful, and while there are definitely a bunch more techniques for debugging, these are the ones I always turn to first and usually are all I need to work out what is happening. I will (one day) get RubyMine's built in debugger working and report back if it blows any of this out of the water :D
in posts
Tagged