wasabigeek

Reading a Ruby gem with VSCode

February 15, 2022

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.

Open the Source Code

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.

For example EDITOR=code bundle open mocha from my project opens the gem in VSCode:

Mocha opened in VSCode.png

If the gem isn’t installed, you could also look up the source code online (rubygems.org) and if the code is on GitHub, there’s a handy shortcut to open it in a browser version of VSCode.

Find your Target

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:

Searching Mocha in VSCode.png

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_instance would 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 any_instance_method and mock_impersonating_any_instance_of in 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

Trace the Callers

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:

VSCode Go To References in Mocha.png

Above, we find that Mocha::ClassMethods is included into Class in Mocha::API:

Class.send(:include, ClassMethods)

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 require-d.

Know the Structure of a Gem

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 mocha:

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!).

Footnotes

Related Posts

Practical Metaprogramming in Ruby: minitest/mock

Mocking in RSpec VS Minitest: A Cheatsheet

Comments


Nick

By Nick, a Business grad turned Software Engineer, living in sunny 🇸🇬. I write mostly about Ruby and Rails, not wasabi (sorry!).