Patch - Why won't the relative patch target name work? - python

I've imported a class from a module, but when I try to patch the class name without it's module as a prefix I get a type error:
TypeError: Need a valid target to patch. You supplied: 'MyClass'
For example, the following code gives me the above error:
import unittest
from mock import Mock, MagicMock, patch
from notification.models import Channel, addChannelWithName, deleteChannelWithName, listAllChannelNames
class TestChannel(unittest.TestCase):
#patch("Channel")
def testAddChannelWithNamePutsChannel(self, *args):
addChannelWithName("channel1")
Channel.put.assert_called_with()
While this second version of the code does not give me the type error:
import unittest
from mock import Mock, MagicMock, patch
from notification.models import Channel, addChannelWithName, deleteChannelWithName, listAllChannelNames
class TestChannel(unittest.TestCase):
#patch("notification.models.Channel")
def testAddChannelWithNamePutsChannel(self, *args):
addChannelWithName("channel1")
Channel.put.assert_called_with()
Why is that? Why can I reference Channel as just "Channel" in other places, yet for the patch I need the module prefix not to get an error? Also, I have a feeling that giving the full module prefix isn't working either because when I call Channel.put.assert_called_with() I get the error that assert_called_with is not an attribute of Channel.put. Can someone explain what's going on? Thank you much!

The patch decorator requires the target to be a full dotted path, as stated in the documentation:
target should be a string in the form ‘package.module.ClassName’. The target is imported and the specified object replaced with the new object, so the target must be importable from the environment you are calling patch from. The target is imported when the decorated function is executed, not at decoration time.
"Channel" is just a string, and patch does not have enough information to find the proper class. This is not the same as the name Channel you use elsewhere, which is imported at the top of the module.
The second test fails because Channel gets imported in the test module then patch replaces Channel in notification.models with a mock object. What patch actually does is change the object the name Channel used inside notification.models point to. The name Channel in the test module has already been defined, so it is not affected. This is actually better explained here: https://docs.python.org/3/library/unittest.mock.html#where-to-patch
To access the patched version of your object, you can either access the module directly:
import unittest
from unittest.mock import patch
from notification.models import Channel, addChannelWithName
from notification import models
class TestChannel1(unittest.TestCase):
#patch("notification.models.Channel")
def testAddChannelWithNamePutsChannel(self, *args):
addChannelWithName("channel1")
models.Channel.put.assert_called_with("channel1")
Or use the patched version passed as an extra argument to the decorated function:
class TestChannel2(unittest.TestCase):
#patch("notification.models.Channel")
def testAddChannelWithNamePutsChannel(self, mock_channel):
addChannelWithName("channel1")
mock_channel.put.assert_called_with("channel1")
If you just want to quickly patch a single method on an object, it's usually easier to use the patch.object decorator:
class TestChannel3(unittest.TestCase):
#patch.object(Channel, 'put')
def testAddChannelWithNamePutsChannel(self, *arg):
addChannelWithName("channel1")
Channel.put.assert_called_with("channel1")

Related

How do I patch a python #classmethod to call my side_effect method?

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()
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(...).

Trying to mock patch a function, but getting PIL\\Image.py'> does not have the attribute 'save'

I am using pytest-mock, but the exception being thrown is from the mock.patch code, I have verified that the same errot occurs if I use the #mock.patch decorator syntax.
MRE (ensure you have pytest and pytest-mock installed, no need to import anything):
def test_image(mocker):
mocker.patch("PIL.Image.save")
Now run pytest on this module.
Error:
E AttributeError: <module 'PIL.Image' from 'c:\\users\\...\\site-packages\\PIL\\Image.py'> does not have the attribute 'save'
I can see clearly that Image.py does contain a function called save, but are functions not considered attributes? I've never heard that word used for the contents of a module.
save is an instance method of PIL.Image.Image class and not PIL.Image module.
You should implement the patch as:
def test_image(mocker):
mocker.patch("PIL.Image.Image.save")
If you need to make assertions that the save method is invoked on the Image instance, you need a name that is bound to the mock.
You can implement that by mocking the Image class and binding a Mock instance to its save method . For example,
def test_image(mocker):
# prepare
klass = mocker.patch("PIL.Image.Image")
instance = klass.return_value
instance.save = mocker.Mock()
# act
# Do operation that invokes save method on `Image` instance
# test
instance.save.assert_called()

Patching an object by reference rather than by name string?

The most common way to patch something in a module seems to be to use something like
from unittest.mock import patch
from mypackage.my_module.my_submodule import function_to_test
#patch('mypackage.my_module.my_submodule.fits.open')
def test_something(self, mock_fits_open)
# ...
mock_fits_open.return_value = some_return_value
function_to_test()
# ...
However, with the value passed to the patch decorator being a string, I don't get lots of the nice benefits from IDE. I can't use parts of the string to jump to definitions. I don't get autocomplete (and an implicit spelling check). Nor full refactoring capabilities. And so on.
Using patch.object I can get much closer to what I'm looking for.
from unittest.mock import patch
import mypackage.my_module.my_submodule
from mypackage.my_module.my_submodule import function_to_test
#patch.object(mypackage.my_module.my_submodule.fits, 'open')
def test_something(self, mock_fits_open)
# ...
mock_fits_open.return_value = some_return_value
function_to_test()
# ...
However, this still requires the final part of the name of the referenced object is just a string. Is there a (nice) way to patch an object purely on the reference to that object? That is, I would like to be able to do something like
from unittest.mock import patch
import mypackage.my_module.my_submodule
from mypackage.my_module.my_submodule import function_to_test
#patch.reference(mypackage.my_module.my_submodule.fits.open)
def test_something(self, mock_fits_open)
# ...
mock_fits_open.return_value = some_return_value
function_to_test()
# ...
Patching works by replacing in the namespace where the name is looked up.
The underlying logic of mock.patch is essentially working with a context-managed name shadowing. You could do the same thing manually with:
save original value associated with name (if any)
try overwriting the name
execute the code under test
finally resetting name back to the original value
Therefore, you fundamentally need to patch on a name, there is no patching a reference directly.
You can use the __name__ attribute of the function object:
from unittest.mock import patch
import my_module
#patch.object(my_module, my_module.my_fun.__name__)
def test_something(self, mocked_fun)
# ...
See also: Name of a Python function in a stack trace

Mocking a module level function in pytest

I have a function that has a decorator. The decorator accepts arguments and the value of the argument is derived from another function call.
example.py
from cachetools import cached
from cachetools import TTLCache
from other import get_value
#cached(cache=TTLCache(maxsize=1, ttl=get_value('cache_ttl')))
def my_func():
return 'result'
other.py
def get_value(key):
data = {
'cache_ttl': 10,
}
# Let's assume here we launch a shuttle to the space too.
return data[key]
I'd like to mock the call to get_value(). I'm using the following in my test:
example_test.py
import mock
import pytest
from example import my_func
#pytest.fixture
def mock_get_value():
with mock.patch(
"example.get_value",
autospec=True,
) as _mock:
yield _mock
def test_my_func(mock_get_value):
assert my_func() == 'result'
Here I'm injecting mock_get_value to test_my_func. However, since my decorator is called on the first import, get_value() gets called immediately. Any idea if there's a way to mock the call to get_value() before module is imported right away using pytest?
Move the from example import my_func inside your with in your test function. Also patch it where it's really coming from, other.get_value. That may be all it takes.
Python caches modules in sys.modules, so module-level code (like function definitions) only runs on the first import from anywhere. If this isn't the first time, you can force a re-import using either importlib.reload() or by deleting the appropriate key in sys.modules and importing again.
Beware that re-importing a module may have side effects, and you may also want to re-import the module again after running the test to avoid interfering with other tests. If another module was using objects defined in the re-imported module, these don't just disappear, and may not be updated the way it expects. For example, re-importing a module may create a second instance of what was supposed to be a singleton.
One more robust approach would be save the original imported module object somewhere else, delete from sys.modules, re-import with the patched version for the duration of the test, and then put back the original import into sys.modules after the test. You could do this with an import inside of a patch.dict() context on sys.modules.
import mock
import sys
import pytest
#pytest.fixture
def mock_get_value():
with mock.patch(
"other.get_value",
autospec=True,
) as _mock, mock.patch.dict("sys.modules"):
sys.modules.pop("example", None)
yield _mock
def test_my_func(mock_get_value):
from example import my_func
assert my_func() == 'result'
Another possibility is to call the decorator yourself in the test, on the original function. If the decorator used functools.wraps()/functools.update_wrapper(), then original function should be available as a __wrapped__ attribute. This may not be available depending on how the decorator was implemented.

monkeypatch function in module whose namespace was overwritten

I am trying to monkeypatch a function in an external module I use, but monkeypatch can't seem to access the function because the namespace of the module gets overwritten on import.
Concretely, I use a Bio.PDB.PDBList.PDBList object (biopython module) in my code, and I am trying to patch _urlretrieve in Bio.PDB.PDBList to prevent calls to the internet and instead get files from a local directory, without having to mock the instance methods of PDBList which would be substantially more work. But when I try the naïve:
m.setattr("Bio.PDB.PDBList._urlretrieve", mock_retrieve)
pytest complains:
AttributeError: 'type' object at Bio.PDB.PDBList has no attribute '_urlretrieve'
On further inspection of Bio.PDB, I can see that the module namespace .PDBList seems to be overwritten by the class .PDBList.PDBList:
# Download from the PDB
from .PDBList import PDBList
So that would explain why pytest sees Bio.PDB.PDBList as a type object with no attribute _urlretrieve. My question is, is there any way to get monkeypatch to patch this 'hidden' function?
Concrete example of usage of PDBList class:
from Bio.PDB.PDBList import PDBList
_pdblist = PDBList()
downloaded_file = _pdblist.retrieve_pdb_file('2O8B', pdir='./temp', file_format='pdb')
You are right - since the PDBList class has the same name as the module Bio.PDB.PDBList, after import Bio.PDB.PDBList you won't be able to access the module by its name (shadowing problem). However, you can still grab the imported module object from the loaded modules cache and monkeypatch that:
import sys
from unittest.mock import Mock
import Bio.PDB.PDBList
def test_spam(monkeypatch):
assert isinstance(Bio.PDB.PDBList, type)
with monkeypatch.context() as m:
m.setattr(sys.modules['Bio.PDB.PDBList'], '_urlretrieve', Mock())
...

Categories