# The Beginner's Guide To Monkey Patching In Ruby

> This guide will alleviate questions that new Rubyists have around monkey patching and how it works in Ruby.

- Canonical: https://dennisokeeffe.com/blog/2022-02-17-the-beginners-guide-to-monkey-patching-in-ruby
- Published: 2022-02-17
- Updated: 2022-02-17
- Tags: 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](https://github.com/okeeffed/demo-monkey-patching-in-ruby)

## Prerequisites

1. Basic familiarity with [Bundler](https://bundler.io/).
2. Familiarity with reading tests written in [RSpec](https://rspec.info/).
3. Familiarity with the [Pry Gem](https://www.rubydoc.info/gems/pry/0.10.3/Pry).

## Getting started

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

```s
$ 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:

```rb
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](https://blog.daveallie.com/include-vs-prepend) 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:

```rb
# 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](https://www.geeksforgeeks.org/monkey-patching-in-ruby/).

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

```rb
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:

```rb
$ 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`.

```s
$ 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](https://www.justinweiss.com/articles/3-ways-to-monkey-patch-without-making-a-mess/) 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](https://guides.rubyonrails.org/active_support_core_extensions.html), 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:

```rb
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`.

```rb
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:

```rb
$ 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:

```rb
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.

```s
[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:

```s
[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

- [Bundler](https://bundler.io/)
- [RSpec](https://rspec.info/)
- [Pry Gem](https://www.rubydoc.info/gems/pry/0.10.3/Pry)
- [Source code](https://github.com/okeeffed/demo-monkey-patching-in-ruby)

_Photo credit: [`joshstyle`](https://unsplash.com/@joshstyle)_
