Un-monkey patching a class/method in Ruby
-
11-02-2021 - |
Domanda
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.
Soluzione
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
Altri suggerimenti
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
or you can use a stubbing framework (like rspec or mocha) and stub the File.Open method.
File.stub(:open => "0\n")
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
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