I am writing some unit tests (using pytest) for someone else's code which I am not allowed to change or alter in any way. This code has a global variable, that is initialized with a function return outside of any function and it calls a function which (while run locally) raises an error. I cannot share that code, but I've coded a simple file that has the same problem:
def annoying_function():
'''Does something that generates exception due to some hardcoded cloud stuff'''
raise ValueError() # Simulate the original function raising error due to no cloud connection
annoying_variable = annoying_function()
def normal_function():
'''Works fine by itself'''
return True
And this is my test function:
def test_normal_function():
from app.annoying_file import normal_function
assert normal_function() == True
Which fails due to ValueError from annoying_function, because it is still called during the module import.
Here's the stack trace:
failed: def test_normal_function():
> from app.annoying_file import normal_function
test\test_annoying_file.py:6:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
app\annoying_file.py:6: in <module>
annoying_variable = annoying_function()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def annoying_function():
'''Does something that generates exception due to some hardcoded cloud stuff'''
> raise ValueError()
E ValueError
app\annoying_file.py:3: ValueError
I have tried mocking this annoying_function like this:
def test_normal_function(mocker):
mocker.patch("app.annoying_file.annoying_function", return_value="foo")
from app.annoying_file import normal_function
assert normal_function() == True
But the result is the same.
Here's the stack trace:
failed: thing = <module 'app' (<_frozen_importlib_external._NamespaceLoader object at 0x00000244A7C72FE0>)>
comp = 'annoying_file', import_path = 'app.annoying_file'
def _dot_lookup(thing, comp, import_path):
try:
> return getattr(thing, comp)
E AttributeError: module 'app' has no attribute 'annoying_file'
..\..\..\..\.pyenv\pyenv-win\versions\3.10.5\lib\unittest\mock.py:1238: AttributeError
During handling of the above exception, another exception occurred:
mocker = <pytest_mock.plugin.MockerFixture object at 0x00000244A7C72380>
def test_normal_function(mocker):
> mocker.patch("app.annoying_file.annoying_function", return_value="foo")
test\test_annoying_file.py:5:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv\lib\site-packages\pytest_mock\plugin.py:440: in __call__
return self._start_patch(
.venv\lib\site-packages\pytest_mock\plugin.py:258: in _start_patch
mocked: MockType = p.start()
..\..\..\..\.pyenv\pyenv-win\versions\3.10.5\lib\unittest\mock.py:1585: in start
result = self.__enter__()
..\..\..\..\.pyenv\pyenv-win\versions\3.10.5\lib\unittest\mock.py:1421: in __enter__
self.target = self.getter()
..\..\..\..\.pyenv\pyenv-win\versions\3.10.5\lib\unittest\mock.py:1608: in <lambda>
getter = lambda: _importer(target)
..\..\..\..\.pyenv\pyenv-win\versions\3.10.5\lib\unittest\mock.py:1251: in _importer
thing = _dot_lookup(thing, comp, import_path)
..\..\..\..\.pyenv\pyenv-win\versions\3.10.5\lib\unittest\mock.py:1240: in _dot_lookup
__import__(import_path)
app\annoying_file.py:6: in <module>
annoying_variable = annoying_function()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def annoying_function():
'''Does something that generates exception due to some hardcoded cloud stuff'''
> raise ValueError()
E ValueError
app\annoying_file.py:3: ValueError
Also moving the import statement around doesn't affect my result.
From what I've read this happens because the mocker (I'm using pytest-mock) has to import the file with the function it is mocking, and during the import of this file the line annoying_variable = annoying_function() runs and in result fails the mocking process.
The only way that I've found to make this sort-of work is by mocking the cloud stuff that's causing the error in the original code, but I want to avoid this as my tests kind of stop being unit tests then.
Again, I can't modify or alter the original code. I'll be gratefull for any ideas or advice.
As other commenters already noted, the problem that you attempt to solve hints at bigger issues with the code to be tested, so probably giving an answer to how the specific problem can be solved is actually the wrong thing to do. That said, here is a bit of an unorthodox and messy way to do so. It is based on the following ideas:
annoying_file.pydynamically before importing it, so thatannoying_function()will not be called. Given your code example, we can achieve this, for example, by replacingannoying_variable = annoying_function()withannoying_variable = Nonein the actual source code.normal_function()in the dynamically adjusted module.In the following code, I assume that
annoying_file.pycontains theannoying_function(),annoying_variable, andnormal_function()from your question,annoying_file.pyand the module that contains the code below live in the same folder.The code achieves the following:
patch_annoying_variable_in(), the original code ofannoying_fileis parsed. The assignment toannoying_variableis replaced, so thatannoying_function()will not be executed. The resulting adjusted source code is returned.import_from(), the adjusted source code is loaded as a module.test_normal_function()makes use of the previous two functions to test the dynamically adjusted module.