Back to home

The Beginner's Guide To Monkey Patching In Ruby main image

The Beginner's Guide To Monkey Patching In Ruby

"Monkey patching" is used (and misused) a lot in Ruby. It is a way of adding and overriding methods to existing classes without having to rewrite the entire class.

This post looks to demonstrate how monkey patching works in Ruby, as well as some best practices found from across the community.

Source code can be found here

Prerequisites

  1. Basic familiarity with Bundler.
  2. Familiarity with reading tests written in RSpec.
  3. Familiarity with the Pry Gem.

Getting started

We will create the project directory demo-monkey-patching-in-ruby and use Bundler to initialize the project:

$ mkdir demo-monkey-patching-in-ruby $ cd demo-monkey-patching-in-ruby # Create the required folders and files # Our recursive creation for the extensions $ mkdir -p lib/core_extensions/monkey_patching $ touch lib/monkey_patching.rb lib/core_extensions/monkey_patching/basic.rb # Make the test folder $ mkdir spec $ touch spec/monkey_patching_spec.rb # Initialise Bundler project $ bundler init # RSpec added for testing $ bundler add rspec --group "development,test" # Pry added for debugging $ bundler add pry --group "development,test"

At this stage, our project is now ready to start working with.

Our basic class

Within the lib/monkey_patching.rb file, let's add the following code:

class MonkeyPatching def hello 'world' end def yell 'gaaah!' end def extend 'extended' end end

This class is contrived for the sake of our demonstration, but in this post we will be aiming for the following:

  1. An example of the hello method being overwritten by a monkey patch.
  2. Demonstrating that yell will remain unaffected after the class is monkey patched.
  3. An example of extending the functionality of the extend method by using monkey patching.

Before we write out our extension file, it is best to do a quick pivot to understand why we will opt to use prepend instead of include for the purpose of monkey patching in Ruby.

include vs prepend

Dave Allie has an excellent post demonstrating the ancestral chain of classes when using include vs prepend.

It is best to read the post through, but as a simple example, here is the relevant ancestral demonstration:

# For `include` module IncludeExt end class IncludeExample include IncludeExt end IncludeExample.ancestors # => [IncludeExample, IncludeExt, Object, Kernel, BasicObject] # For `prepend` module PrependExt end class PrependExample prepend PrependExt end IncludeExample.ancestors # => [PrependExample, PrependExt, Object, Kernel, BasicObject]

The tl;dr from the post most relevant to monkey patching: Use prepend when overwriting/extending an existing class.

If we prepend a module to a class with methods of the same name, we will overwrite the base definition. If we use the super keyword within a prepend, we can call to the original definition and extend its behavior. We will demonstrate both overwriting as well as extending.

Note: there is another alternative where we re-write the class for the monkey patching, but I have opted not to cover the example as it is not an approach I would recommend in the wild. There is an example of this on Geeks for geeks.

The monkey patching spec

Let's write out our spec into spec/monkey_patching.rb so that we can see what our aims our. The approach in these specs are contrived but designed to colocate the examples into one test file:

require 'monkey_patching' require 'core_extensions/monkey_patching/basic' RSpec.describe MonkeyPatching do context 'no monkey patching' do it 'should return "world"' do expect(subject.hello).to eq('world') end it 'should return our gasp when we yell' do expect(subject.yell).to eq('gaaah!') end it 'should return "extended" when we call to extend' do expect(subject.extend).to eq('extended') end end context 'with monkey patching' do before do MonkeyPatching.prepend CoreExtensions::MonkeyPatching::Basic end xit 'should return "people"' do expect(subject.hello).to eq('people') end xit 'should return our gasp when we yell' do expect(subject.yell).to eq('gaaah!') end xit 'should return "I am extended" when we call to extend' do expect(subject.extend).to eq('I am extended') end end end

In our specs, we split up the examples into two contexts:

  1. Without monkey patching.
  2. With monkey patching.

The "With monkey patching" context has a before hook to prepend the module CoreExtensions::MonkeyPatching::Basic that we have included in our file before the tests.

I am also currently using xit to denote the tests that we will skip and work on later.

As things currently stand, if we run bundler exec -- rspec spec/monkey_patching_spec.rb then we should get the following output:

$ bundler exec -- rspec spec/monkey_patching_spec.rb ...*** Pending: (Failures listed here are expected and do not affect your suite's status) 1) MonkeyPatching with monkey patching should return "people" # Temporarily skipped with xit # ./spec/monkey_patching_spec.rb:24 2) MonkeyPatching with monkey patching should return our gasp when we yell # Temporarily skipped with xit # ./spec/monkey_patching_spec.rb:28 3) MonkeyPatching with monkey patching should return "I am extended" when we call to extend # Temporarily skipped with xit # ./spec/monkey_patching_spec.rb:32

Our basic expectations for our standard MonkeyPatching class all pass, and our three tests that are marked xit are not yet run.

If we change our fourth test 'should return "people"' to it and run our tests again, we will see our fourth test fails as subject.hello still returns world.

$ bundler exec -- rspec spec/monkey_patching_spec.rb ...F** Pending: (Failures listed here are expected and do not affect your suite's status) 1) MonkeyPatching with monkey patching should return our gasp when we yell # Temporarily skipped with xit # ./spec/monkey_patching_spec.rb:28 2) MonkeyPatching with monkey patching should return "I am extended" when we call to extend # Temporarily skipped with xit # ./spec/monkey_patching_spec.rb:32 Failures: 1) MonkeyPatching with monkey patching should return "people" Failure/Error: MonkeyPatching.prepend CoreExtensions::MonkeyPatching::Basic NameError: uninitialized constant CoreExtensions # ./spec/monkey_patching_spec.rb:21:in `block (3 levels) in <top (required)>' Finished in 0.00441 seconds (files took 0.12602 seconds to load) 6 examples, 1 failure, 2 pending Failed examples: rspec ./spec/monkey_patching_spec.rb:24 # MonkeyPatching with monkey patching should return "people"

To remedy this, let's move onto writing some code for our extension to fix this problem.

Writing our extension

The file lib/core_extensions/monkey_patching/basic.rb is where we will write our monkey patching code.

The naming convention used here comes from a Rails convention brought up in this blog post by Justin Weiss where states that the Rails convention is to write our monkey patches to the file-naming convention lib/core_extensions/<class_name>/<group>.rb. Feel free to adjust as you see fit for your own project.

Note: My forced interpretation is different to the Rails convention, but for the sake of this post, I will push forward - the example still relays the important parts. The golden rule is to have a set standard and convention that you follow when it comes to defining where your monkey patches live.

Within the file, add the following:

module CoreExtensions module MonkeyPatching module Basic def hello 'people' end def extend "I am #{super}" end end end end

Our code here enables us to fulfill all three of our objectives.

  • hello is overwritten to return 'people'.
  • extend is extended to return "I am #{super}" where super calls to the super class on our ancestor tree: the original MonkeyPatching implementation of extend.

At this stage, we are ready to go back to our test file and change all of our xit tests to it.

require 'monkey_patching' require 'core_extensions/monkey_patching/basic' RSpec.describe MonkeyPatching do context 'no monkey patching' do it 'should return "world"' do expect(subject.hello).to eq('world') end it 'should return our gasp when we yell' do expect(subject.yell).to eq('gaaah!') end it 'should return "extended" when we call to extend' do expect(subject.extend).to eq('extended') end end context 'with monkey patching' do before do MonkeyPatching.prepend CoreExtensions::MonkeyPatching::Basic end it 'should return "people"' do expect(subject.hello).to eq('people') end it 'should return our gasp when we yell' do expect(subject.yell).to eq('gaaah!') end it 'should return "I am extended" when we call to extend' do expect(subject.extend).to eq('I am extended') end end end

If we now run RSpec, we should see the following output:

$ bundler exec -- rspec spec/monkey_patching_spec.rb ...... Finished in 0.00611 seconds (files took 0.15551 seconds to load) 6 examples, 0 failures

Perfect! Our monkey patching code has been successfully applied to our MonkeyPatching class.

We can look deeper at this using binding.pry to see what is happening within our MonkeyPatching class.

Let's adjust our first test to add binding.pry to our test:

require 'pry' # require pry gem require 'monkey_patching' require 'core_extensions/monkey_patching/basic' RSpec.describe MonkeyPatching do context 'no monkey patching' do it 'should return "world"' do expect(subject.hello).to eq('world') end it 'should return our gasp when we yell' do expect(subject.yell).to eq('gaaah!') end it 'should return "extended" when we call to extend' do expect(subject.extend).to eq('extended') end end context 'with monkey patching' do before do MonkeyPatching.prepend CoreExtensions::MonkeyPatching::Basic end it 'should return "people"' do binding.pry # LINE CHANGED HERE expect(subject.hello).to eq('people') end it 'should return our gasp when we yell' do expect(subject.yell).to eq('gaaah!') end it 'should return "I am extended" when we call to extend' do expect(subject.extend).to eq('I am extended') end end end

Now when we run our tests, our debugger will open up in the terminal. We can use the ancestors method to see what is happening within our MonkeyPatching class.

[1] pry(#<RSpec::ExampleGroups::MonkeyPatching::WithMonkeyPatching>)> MonkeyPatching.ancestors => [CoreExtensions::MonkeyPatching::Basic, MonkeyPatching, Object, PP::ObjectMixin, Kernel, BasicObject]

Here we can note that CoreExtensions::MonkeyPatching::Basic is the first ancestor of MonkeyPatching. This is because we have added our monkey patching code to the top of our class.

The super ancestor of CoreExtensions::MonkeyPatching::Basic is MonkeyPatching, so this is what helps us to access the original implementation of extend.

Below are some more debugging calls to show what is returned by binding.pry to aid in demonstrating what our MonkeyPatching subject has as return values when those methods are invoked:

[2] pry(#<RSpec::ExampleGroups::MonkeyPatching::WithMonkeyPatching>)> subject.extend => "I am extended" [3] pry(#<RSpec::ExampleGroups::MonkeyPatching::WithMonkeyPatching>)> subject.hello => "people" [4] pry(#<RSpec::ExampleGroups::MonkeyPatching::WithMonkeyPatching>)> subject.yell => "gaaah!"

Summary

Today's post demonstrated how to use the prepend method to add monkey patching code to a class.

Monkey patching can be useful for augmenting functionality to classes in Ruby, but you must implement them with caution to ensure that you don't break the original functionality or make the implementation of monkey patches confusing and messy.

Resources and further reading

Photo credit: joshstyle

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.