0

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()
OldSchool
  • 459
  • 1
  • 3
  • 14
  • Does this answer your question: https://stackoverflow.com/a/38579854/13891412 ? – Clasherkasten Dec 24 '22 at 20:55
  • Nice at @Clasherkasten. If I replace `side_effect=` with `new=` for the class_method case, it seems to work. Not sure I understand it though. When would one use `side_effect=` and when `new=` in general? – OldSchool Dec 24 '22 at 21:34
  • I don't have any clue about that either, sorry. I just found that answer, tested it and suggested it cause it seemed to work. – Clasherkasten Dec 24 '22 at 21:36
  • Ah ok, fair enough. However, although it works now when calling from the `obj` handle, it doesn't seem to work when attempting to call the @classmethod from its class handle. i.e. calling `SomeClass.class_method()` inside the `with` block still fails with `TypeError: class_method() missing 1 required positional argument: 'cls'` – OldSchool Dec 24 '22 at 21:47
  • `new=classmethod(class_method)` seems to make it work again :) – Clasherkasten Dec 24 '22 at 21:50
  • Once again, niiice! That seems to successfully patch both `obj.class_method()` and `SomeClass.class_method()`. It only works with `new` and not `side_effect`. Would be great to know the difference between those two. In the meantime, if you want to answer I'll upvote you and check as the correct answer – OldSchool Dec 24 '22 at 21:56
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/250635/discussion-between-oldschool-and-clasherkasten). – OldSchool Dec 24 '22 at 21:58

1 Answers1

1

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(...).
Clasherkasten
  • 488
  • 3
  • 9
  • Thank you for putting this answer together @Clasherkasten. I'm not very good with this bit of the language. It would be great to find out whether or not there is a way to patch class methods whilst retaining the Mock properties of the created object/class. I am nevertheless up voting your answer and marking it as correct because it does solve the problem I had as stated in the question. Thank you! – OldSchool Dec 28 '22 at 23:29