Search code examples
pythonmockingpatch

How do I patch a python @classmethod to call my side_effect method?


The following code shows the problem.

I can successfully patch object instance and static methods of this SomeClass

However, I can't seem to be able to patch classmethods.

Help much appreciated!

from contextlib import ExitStack
from unittest.mock import patch


class SomeClass:
    def instance_method(self):
        print("instance_method")

    @staticmethod
    def static_method():
        print("static_method")

    @classmethod
    def class_method(cls):
        print("class_method")

# --- desired patch side effect methods ----
def instance_method(self):
    print("mocked instance_method")

def static_method():
    print("mocked static_method")

def class_method(cls):
    print("mocked class_method")

# --- Test ---
obj = SomeClass()

with ExitStack() as stack:
    stack.enter_context(
        patch.object(
            SomeClass,
            "instance_method",
            side_effect=instance_method,
            autospec=True
        )
    )
    stack.enter_context(
        patch.object(
            SomeClass,
            "static_method",
            side_effect=static_method,
            # autospec=True,
        )
    )
    stack.enter_context(
        patch.object(
            SomeClass,
            "class_method",
            side_effect=class_method,
            # autospec=True
        )
    )


    # These work
    obj.instance_method()
    obj.static_method()

    # This fails with TypeError: class_method() missing 1 required positional argument: 'cls'
    obj.class_method()

Solution

  • General solution

    A way to patch a classmethod would be to use new=classmethod(class_method) instead of side_effects=class_method.
    This works pretty well in general.

    Downside

    Using new, the patched object isn't necessarily an instance of Mock, MagicMock, AsyncMock or PropertyMock anymore (During the rest of the answer i'll only reference Mock as all the others are subclasses of it).
    It is only then an instance of these when you explicitly specify it to be one via e.g. new=Mock(...) or ommit the attribute completely.
    That wouldn't be the case with the solution provided at the top of this answer. So when you try to e.g. check if the function already got called using obj.class_method.assert_called(), it'll give an error saying that function has no attribute assert_called which is caused by the fact that the patched object isn't an instance of Mock, but instead a function.

    Unfortunately I don't see any solution to this downside in that scenario at the moment

    Concluded differences between new and side_effect:

    • new specifies what object to patch the target with (doesn't necessarily have to be an instance of Mock)
    • side_effect specifies the side_effect of the Mock instance that gets created when using patch without new Also they don't play very well together, so only one of these can/should be used in the same patch(...).