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
includedoes 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
ancestorsofA's singleton class:When including
B::ClassMethodsintoClass, this list changes accordingly:Note that
B::ClassMethodswas added afterClass.Now, if you "undefine" method
aviaundef/undef_methodwithClassas its receiver, it will causeClassto prevent calls to that method by raising an (artificial)NoMethodErrorwhich also ends further method lookup: (I say "artificial" because the method is still there)If you include another module
C::ClassMethodsintoClass, it will be added to the list, in front ofB::ClassMethodsbut still afterClass:And since
Classstill preventsafrom being called, the newamethod 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_instancemethods are defined.You can then add a monkey-patch into the right place via
include/prepend.