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
of A
's singleton class:
class A; end
A.singleton_class.ancestors
#=> [#<Class:A>, #<Class:Object>, #<Class:BasicObject>,
# Class, Module, Object, Kernel, BasicObject]
When including B::ClassMethods
into Class
, this list changes accordingly:
Class.include(B::ClassMethods)
A.singleton_class.ancestors
#=> [#<Class:A>, #<Class:Object>, #<Class:BasicObject>,
# Class, B::ClassMethods, Module, Object, Kernel, BasicObject]
# ^^^^^^^^^^^^^^^
Note that B::ClassMethods
was added after Class
.
Now, if you "undefine" method a
via undef
/ undef_method
with Class
as its receiver, it will cause Class
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)
#<Class:A> → ... → Class → B::ClassMethods → ...
| |
undef a a
(never gets here)
If you include another module C::ClassMethods
into Class
, it will be added to the list, in front of B::ClassMethods
but still after Class
:
Class.include(C::ClassMethods)
A.singleton_class.ancestors
#=> [#<Class:A>, #<Class:Object>, #<Class:BasicObject>,
# Class, C::ClassMethods, B::ClassMethods, Module, Object, Kernel, BasicObject]
# ^^^^^^^^^^^^^^^
And since Class
still prevents a
from being called, the new a
method can't be reached either:
#<Class:A> → ... → Class → C::ClassMethods → B::ClassMethods → ...
| | |
undef a a a
(still not gets here)
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
.