Un-monkey patching a class/method in Ruby

4.4k views Asked by At

I'm trying to unit test a piece of code that I've written in Ruby that calls File.open. To mock it out, I monkeypatched File.open to the following:

class File
  def self.open(name, &block)
    if name.include?("retval")
      return "0\n"
    else
      return "1\n"
    end
  end
end

The problem is that I'm using rcov to run this whole thing since it uses File.open to write code coverage information, it gets the monkeypatched version instead of the real one. How can I un-monkeypatch this method to revert it to it's original method? I've tried messing around with alias, but to no avail so far.

5

There are 5 answers

1
peakxu On BEST ANSWER

Expanding on @Tilo's answer, use alias again to undo the monkey patching.

Example:

# Original definition
class Foo
  def one()
    1
  end
end

foo = Foo.new
foo.one

# Monkey patch to 2
class Foo
  alias old_one one
  def one()
    2
  end
end

foo.one

# Revert monkey patch
class Foo
  alias one old_one
end

foo.one
1
Matheus Moreira On

File is just a constant that holds an instance of Class. You could set it to a temporary class that responds to open, and then restore the original one:

original_file = File
begin
  File = Class.new             # warning: already initialized constant File
  def File.open(name, &block)
    # Implement method
  end
  # Run test
ensure
  File = original_file         # warning: already initialized constant File
end
2
Dan On

or you can use a stubbing framework (like rspec or mocha) and stub the File.Open method.

File.stub(:open => "0\n")

3
Tilo On

you can simply alias it like this:

alias new_name old_name

e.g.:

class File
  alias old_open open

  def open
    ...
  end
end

now you can still access the original File.open method via File.old_open


Alternatively, you could try something like this:

ruby - override method and then revert

http://blog.jayfields.com/2006/12/ruby-alias-method-alternative.html

0
Kelvin On

The correct way to do this is to actually use a stubbing framework like Dan says.

For example, in rspec, you'd do:

it "reads the file contents" do
  File.should_receive(:open) {|name|
    if name.include?("retval")
      "0\n"
    else
      "1\n"
    end
  }
  File.open("foo retval").should == "0\n"
  File.open("other file").should == "1\n"
end

But for the curious, here's a semi-safe way to do it without external libs. The idea is to isolate the stub so as little code is affected by it as possible.

I named this script isolated-patch.rb:

class File
  class << self
    alias_method :orig_open, :open

    def stubbed_open(name, &block)
      if name.include?("retval")
        return "0\n"
      else
        return "1\n"
      end
    end
  end
end


def get_size(path)
  # contrived example
  File.open(path) {|fh|
    # change bytesize to size on ruby 1.8
    fh.read.bytesize
  }
end

# The stub will apply for the duration of the block.
# That means the block shouldn't be calling any methods where file opening
#   needs to work normally.
# Warning: not thread-safe
def stub_file_open
  class << File
    alias_method :open, :stubbed_open
  end
  yield
ensure
  class << File
    alias_method :open, :orig_open
  end
end

if __FILE__ == $0
  stub_file_open do
    val = File.open("what-retval") { puts "this is not run" }
    puts "stubbed open() returned: #{val.inspect}"
    size = get_size("test.txt")
    puts "obviously wrong size of test.txt: #{size.inspect}"
  end

  # you need to manually create this file before running the script
  size = get_size("test.txt")
  puts "size of test.txt: #{size.inspect}"
end

Demo:

> echo 'foo bar' > test.txt
> ruby isolated-patch.rb
stubbed open() returned: "0\n"
obviously wrong size of test.txt: "1\n"
size of test.txt: 8