Whether out of curiosity or trying to understand a method for debugging, it’s helpful to know how to dig into a Ruby gem. Recently I was curious over how mocha, a mocking library for minitest, allowed stubbing on
any_instance of a Class (as opposed to injecting a stubbed object), for example:
def test_stubbing_an_instance_method_on_all_instances_of_a_class Product.any_instance.stubs(:name).returns('stubbed_name') product = Product.new assert_equal 'stubbed_name', product.name end
Following this are steps you can try when investigating a gem. I’d love to say these were the exact steps I took while investigating
any_instance, but truth is some steps I discovered only while writing this post 😅. The steps are specific to using VSCode.
Assuming the gem is installed,
gem open <gemname> (or
bundle open <gemname> if installed in a project via Bundler) will open it’s source code with the command set in the
$EDITOR environment variable.
EDITOR=code bundle open mocha from my project opens the gem in VSCode:
One of the easiest ways to find the method definition is to use
#source_location. Fire up an irb console:
require 'minitest' require 'mocha/minitest' Object.method(:any_instance).source_location # => ["/.../ruby/3.0.3/gems/mocha-1.13.0/lib/mocha/class_methods.rb", 45]
In case that doesn’t work, since we have the source code open, we can make use of VSCode’s search:
Some ideas to try if the results list is really long:
- prefixing the search term with the declaration syntax (e.g.
class <ClassName>for a class,
def <method_name>for an instance method) generally works well, but not always, given the many ways to metaprogram in Ruby. For example, it’s not obvious that
any_instancewould have been defined as an instance method.
- ”Match Whole Word” (ab) helps exclude classes / methods with the search term as a substring (e.g. it would have excluded
mock_impersonating_any_instance_ofin the results above)
- ”files to include” knowing the structure of most gems (see later section), we can usually restrict to the
lib/directory, which will exclude test files, bin scripts etc
While we found the method, it didn’t answer our initial question, which was how
any_instance becomes callable on any class. We could probably try a “top-down” or “bottom-up” search here.
For “bottom-up” search, since this is plain Ruby as opposed to Rails, we could try to follow the
require trail. But if we have an extension like solargraph, a simpler way is to make use of VSCode’s Go to References:
Above, we find that
Mocha::ClassMethods is included into Class in
We can continue doing the same approach, but at some point the “search tree” got quite unwieldy so I switched to a more “top-down” approach, checking the README to see how the gem was
A quick detour on how a gem is structured -
lib/ generally contains the gem’s core logic. There you’ll find a ruby file and a folder corresponding to the name of the gem. For example, in
lib/ └── mocha.rb └── mocha/
The ruby file (e.g.
lib/mocha.rb) generally contains most of the gem’s bootstrapping logic, as this is the file loaded when requiring the gem (i.e.
require 'mocha' would load
lib/mocha.rb). So typically we’d start our “top-down” search here.
mocha is slightly different - it supports multiple test libraries, and in the README we are instructed to require a different file depending on our test library. For example, for
minitest we’d do:
require 'minitest/unit' require 'mocha/minitest'
So in this case we’d start our search from
lib/mocha/minitest.rb. There on it’s pretty straightforward Ruby, leading us back to
Mocha::API (albeit with knowledge of how it gets required!).
- How to Dispel Ruby Magic and Understand Your Gems (Justin Weiss) - an inspiration for much of this
- Structure of a Gem (RubyGems) - more details on how a gem is structured, along with a quick start so you can build your own gem!
- VSCode docs on searching across files