I'm upgrading a Rails project and had included Mocha in an earlier version. It defined an any_instance
method. The new(er) version of Rspec to which I'm upgrading also includes an any_instance
method on all classes. I need to stick with Mocha for a little while while I upgrade everything so I don't have to change a bunch of tests, so I want to use the Mocha version of any_instance
.
To do that, with Rspec, I'm first removing its own monkey pactching with disable_monkey_patching!
. Then there's a config.mock_with :mocha
call for Rspec that will cause mocha to define any_instance
on Class
. I know that monkey patching all classes is bad, but this question is actually a good lesson in why, but I'm curious about the results I'm seeing.
The above is some background about why I'm doing this. What follows is a minimal reproducible example that I can't explain and would appreciate insight on.
# Define a class
class A; end
# Define a module whose class methods I'd like to include in every class
module B
module ClassMethods
def a; end
end
end
# Include method "a" in all classes
Class.send :include, B::ClassMethods
# Try it out!
A.a # <- works
Now I'll use undef to get rid of it, because that's what disable_monkey_patching!
does:
Class.class_exec { undef a }
A.a # <- undefined method `a' for A:Class (NoMethodError) -- that's expected
But now I need to define a different method "a" for Class
, which I'll define in module C
.
module C
module ClassMethods
def a; end
end
end
Class.send :include, C::ClassMethods
Here's the part that confuses me:
A.a # <- undefined method `a' for A:Class (NoMethodError)
This makes it seem like undef
will permanently undefine it, but will not warn anyone when they try to define a method that will ultimately unusable. Why does this happen?
Tried on MRI 3.2.2 and 2.7.2
Calling
include
does not copy the methods into the receiver. It merely adds the included module to the list of modules that are traversed for method lookup.We can see this list by inspecting the
ancestors
ofA
's singleton class:When including
B::ClassMethods
intoClass
, this list changes accordingly:Note that
B::ClassMethods
was added afterClass
.Now, if you "undefine" method
a
viaundef
/undef_method
withClass
as its receiver, it will causeClass
to prevent calls to that method by raising an (artificial)NoMethodError
which also ends further method lookup: (I say "artificial" because the method is still there)If you include another module
C::ClassMethods
intoClass
, it will be added to the list, in front ofB::ClassMethods
but still afterClass
:And since
Class
still preventsa
from being called, the newa
method can't be reached either:For your actual problem (Mocha), you should first check the ancestors of your object(s) and identify where both
any_instance
methods are defined.You can then add a monkey-patch into the right place via
include
/prepend
.