Home > Net >  How to properly mock a function that instantiates a class variable?
How to properly mock a function that instantiates a class variable?

Time:09-30

I have something like this in my source file

# code.py

def some_func():
    # doing some connections and stuff
    return {'someKey': 'someVal'}


class ClassToTest:

    var = some_func()

My test file looks like this... I am trying to mock some_func as I want to avoid creating connections.

# test_code.py

from src.code import ClassToTest


def mock_function():
    return {"someOtherKey": "someOtherValue"}


class Test_Code(unittest.TestCase):

    @mock.patch('src.code.some_func', new=mock_function)
    def test_ClassToTest(self):
        self.assertEqual(ClassToTest.var, {"someOtherKey": "someOtherValue"})

But this doesn't work. On the other hand if var is an instant variable mock works fine. I guess this is due to class variables getting initialized during imports. How do I properly mock some_func before var even gets initialized?

CodePudding user response:

I didn't really understand the need for you to have this mock and test it in this way, but you can try this approach:

#code.py

def some_func():
    # doing some connections and stuff
    return {'someKey': 'someVal'}


class ClassToTest:

    def __init__(self) -> None:
        self.var = some_func()
import code
import unittest
from unittest import mock

def mock_function():
    return {"someOtherKey": "someOtherValue"}



class Test_Code(unittest.TestCase):

    @mock.patch.object(code, attribute='some_func', new=mock_function)
    def test_ClassToTest(self):
        
        self.assertEqual(code.ClassToTest().var, {"someOtherKey": "someOtherValue"})

if __name__ == '__main__':
    unittest.main()

CodePudding user response:

When you imported code.py, there is no active patch yet, so when ClassToTest.var was initialized, it used the original some_func. Only then would the patch for src.code.some_func would take effect which obviously is too late now.

Solution 1

What you can do is to patch some_func and then reload code.py so that it re-initializes the ClassToTest including its attribute var. Thus since we already have an active patch by the time we reload code.py, then ClassToTest.var would be set with the patched value.

  • But we can't do it if both the class and the patched function lives in the same file, so to make it testable move some_func to another file and then just import it.

src/code.py

from src.other import some_func


class ClassToTest:
    var = some_func()

src/other.py

def some_func():
    # doing some connections and stuff
    return {'realKey': 'realValue'}

test_code.py

from importlib import reload
import sys
import unittest
from unittest import mock

from src.code import ClassToTest  # This will always refer to the unpatched version


def mock_function():
    return {"someOtherKey": "someOtherValue"}


class Test_Code(unittest.TestCase):
    def test_real_first(self):
        self.assertEqual(ClassToTest.var, {"realKey": "realValue"})

    @mock.patch('src.other.some_func', new=mock_function)
    def test_mock_then_reload(self):
        # Option 1:
        # import src
        # reload(src.code)

        # Option 2
        reload(sys.modules['src.code'])

        from src.code import ClassToTest  # This will be the patched version
        self.assertEqual(ClassToTest.var, {"someOtherKey": "someOtherValue"})

    def test_real_last(self):
        self.assertEqual(ClassToTest.var, {"realKey": "realValue"})

Output

$ pytest -q 
...                                                                                           [100%]
3 passed in 0.04s

Solution 2

If you don't want the real some_func to be ever called during test, then just reloading isn't enough. What needs to be done is to never import the file containing ClassToTest nor any file that would import it indirectly. Only import it once an active patch for some_func is already established.

from importlib import reload
import sys
import unittest
from unittest import mock

# from src.code import ClassToTest  # Remove this import!!!


def mock_function():
    return {"someOtherKey": "someOtherValue"}


class Test_Code(unittest.TestCase):
    @mock.patch('src.other.some_func', new=mock_function)
    def test_mock_then_reload(self):
        from src.code import ClassToTest  # Move the import here once the patch has taken effect already
        self.assertEqual(ClassToTest.var, {"someOtherKey": "someOtherValue"})
  • Related