mock patch for the entire test session

199 views Asked by At

tl;dr how to have a mock.patch for a @classmethod last an entire test session instead of only within with scope or function scope?

I want to mock patch a class method. However, I would like to run patch.object once instead of once within every test.

Currently, I must call patch.object twice

class MyClass():
    @classmethod
    def print_hello(cls):
        print("hello from the real MyClass.print_hello")

    def do_something(self):
        pass


def mock_print_hello(_cls):
    print("hello from the patched mock_print_hello")


class TestMyClass(unittest.TestCase):
    def test_init(self):
        with mock.patch.object(MyClass, "print_hello", new_callable=mock_print_hello) as patch:
            MyClass.print_hello()
            MyClass()

    def test_do_something(self):
        with mock.patch.object(MyClass, "print_hello", new_callable=mock_print_hello) as patch:
            MyClass.print_hello()
            MyClass().do_something()

However, I would like to only call patch.object once:

class MyClass():
    @classmethod
    def print_hello(cls) :
        print("hello from the real MyClass.print_hello")

    def do_something(self):
        pass


def mock_print_hello(_cls):
    print("hello from the patched mock_print_hello")


class TestMyClass(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        # this patch will not remain after setUpClass returns
        patch = mock.patch.object(MyClass, "print_hello", new_callable=mock_print_hello)

    def test_init(self):
        # this calls the real MyClass.print_hello, not mock_print_hello
        MyClass.print_hello()
        MyClass()

    def test_do_something(self):
        # this calls the real MyClass.print_hello, not mock_print_hello
        MyClass.print_hello()
        MyClass().do_something()

In practice, patching MyClass.new within setUpClass does not last beyond the scope of the function. In other words, the test_init and test_do_something do not call mock_print_hello, they call MyClass.print_hello.

How can I have the mock patch last beyond the scope of one function?

1

There are 1 answers

0
MrBean Bremen On

In order to start patching, you have to either use a decorator or a context manager, or you can call start manually. Just calling patch or patch.object returns a patcher object, it does not start patching.

As you can't use the decorator or context manager in your setup code, you have to start/stop the patcher in the respective setUp/tearDown methods:

class TestMyClass(unittest.TestCase):
    patcher = None

    @classmethod
    def setUpClass(cls):
        cls.patcher = mock.patch.object(MyClass, "print_hello", new_callable=mock_print_hello)
        cls.patcher.start()

    @classmethod
    def tearDownClass(cls):
        cls.patcher.stop()

If you need the patching to persist in the whole module, you can do the same in the respective module functions:

patcher = None

def setUpModule():
    global patcher
    patcher = mock.patch.object(...)
    patcher.start()

def tearDownModule():
    patcher.stop()

As an aside, if you were to use pytest instead of unittest, this could also be done using a context manager in a fixture:

class MyClassTest:
    @pytest.fixture(scope="class", autouse=True)
    def setup(self):
        with mock.patch.object(MyClass, "print_hello", new_callable=mock_print_hello):
            yield

or in the module case:

@pytest.fixture(scope="module", autosue=True)
def setup_module():
    with patch.object(...):
        yield