Say, I have:
class Test
def initialize(m)
@m = m
end
def test
@m
end
end
How can I temporarily make method #test
of all instances (both existing and new ones) of Test
return 113
, and then restore the original method later?
It sounds like such a simple thing, yet I can't find a nice way to achieve it. Probably because of my poor knowledge of Ruby.
What I have found so far is:
# saving the original method
Test.send(:alias_method, :old_test, :test)
# redefining the method
Test.send(:define_method, :test) { 113 }
# restore the original method
Test.send(:alias_method, :test, :old_test)
Which does the job but, as I understand, it would also redefine the existing #old_test
if one existed?.. And it just feels like a hack rather than proper use of metaprogramming?..
Test
)?Test
?I would appreciate if you could describe multiple ways of achieving the same thing, even those that are hard or impractical. Just to give an idea about the flexibility and limitations of metaprogramming in Ruby :)
Thank you very much 🤗
P.S. The reason I started all of this:
I am using gem rack-throttle
to throttle requests starting with /api
, but other urls shouldn't be affected., and I want to test all of this to make sure it works. In order to test the throttling I had to add the middleware to the test environment too. I've successfully tested it (using minitest), however all other tests that test ApiController
shouldn't be throttled because it makes tests take much longer if we need to wait 1 second after each request.
I decided to monkey patch the RequestSpecificIntervalThrottle#allowed?
with { true }
in minitest's #setup
s to temporarily disable throttling for all of those tests, and then reenable it again in #teardown
s (as otherwise the tests testing the throttling itself will fail). I would appreciate if you tell me how you would approach this.
However now that I've already started digging into metaprogramming, I am also just curious how to achieve this (temporarily redefining a method) even if I am not actually going to use it.
You can use instance_method
to get a UnboundMethod
object from any instance method:
class Foo
def bar
"Hello"
end
end
old_method = Foo.instance_method(:bar)
# Redifine the method
Foo.define_method(:bar) do
puts "Goodbye"
end
puts Foo.new.bar # Goodbye
# restore the old method:
Foo.define_method(old_method.name, old_method)
Unbound methods are a reference to the method at the time it was objectified and subsequent changes to the underlying class will not affect the unbound method.
The equivilent for class methods is:
class Foo
def self.baz
"Hello"
end
end
old_method = Foo.method(:baz).unbind
If you want to make the worlds smallest (and perhaps the most useless) stubbing library you could do it with:
class Stubby
def initialize(klass, method_name, &block)
@klass = klass
@old_method = klass.instance_method(method_name)
@klass.define_method(method_name, &block)
end
def restore
@klass.define_method(@old_method.name, @old_method)
end
def run_and_restore
yield
ensure
restore
end
end
puts Foo.new.bar # Hello
Stubby.new(Foo, :bar) do
"Goodbye"
end.run_and_restore do
puts Foo.new.bar # Goodbye
end
puts Foo.new.bar # Hello