Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I patch a class in the same file as a class under test, that is initialized before the test begins?

(Nota bene: This is heavily modified from the original question, to include details I erroneously elided.)

This is the (summarized) file (common.py) I'm testing. It contains a decorator (derived from the Decorum library) that calls a class method on another object(A): I want to patch out A, because that code makes an external call I'm not testing.

from decorum import Decorum


class A:
    @classmethod
    def c(cls):
        pass


class ClassyDecorum(Decorum):
    """Hack to allow decorated instance methods of a class object to run with decorators.
    Replace this once Decorum 1.0.4+ comes out.
    """

    def __get__(self, instance, owner):
        from functools import partial
        return partial(self.call, instance)


class B(Decorum):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def init(self, *args, **kwargs):
        A.c()
        return super().init(*args, **kwargs)

I'd like to @patch class A in my unittest, to isolate and check B.d()'s functionality. This is my unittest (located in test/test_common.py):

class BDecoratedClass(MagicMock):

    @B
    def dummy_func(self):
        return "Success"

class TestB(TestCase):
    @patch('unittest_experiment.A', autospec=True)
    def test_d(self, mock_a):
        b = BDecoratedClass()
        b.dummy_func()
        mock_a.c.assert_called_once_with()  # Fails

Debugging the above, I see that A is never actually mocked: the code proceeds into A's code, so it makes sense that mock_a is never called, and thus the assertion fails. However, I'd like to properly monkey patch A. This approach works if I'm monkey patching an import that exists in common.py, but apparently not if the class is defined there?

Note that I think this is likely an issue of where I'm patching, that is @patch('common.A', autospec=True) should more likely be something like @patch('where.python.actually.finds.A.when.B.calls.A', autospec=True). But I'm very unclear on how to determine if that is the case, and if so, what the correct path is. For instance, @patch('BDecorated.common.A', autospec=True) does not work.

like image 258
Nathaniel Ford Avatar asked Jan 16 '26 19:01

Nathaniel Ford


1 Answers

It looks like patch substitutes the import and that's why it's too late at that point unless you work around it like you explained in your the answer. I found that using patch.object for individual methods also work. So something like:

class TestB(TestCase):
    @patch.object(A, 'c')
    def test_d(self, mock_c):
        b = BDecoratedClass()
        b.dummy_func()
        mock_c.assert_called_once_with()  # Succeeds
like image 68
Katu Avatar answered Jan 19 '26 07:01

Katu



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!