Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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!

like image 498
golmschenk Avatar asked Apr 17 '13 12:04

golmschenk


1 Answers

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") 
like image 185
Nicolas Cortot Avatar answered Nov 05 '22 02:11

Nicolas Cortot