109

I'm trying to mock something while testing a Django app using the imaginatively named Mock testing library. I can't seem to quite get it to work, I'm trying to do this:

models.py

from somelib import FooClass

class Promotion(models.Model):
    foo = models.ForeignKey(FooClass)
    def bar(self):
       print "Do something I don't want!"


test.py

class ViewsDoSomething(TestCase):
    view = 'my_app.views.do_something'

    def test_enter_promotion(self):
        @patch.object(my_app.models.FooClass, 'bar')
        def fake_bar(self, mock_my_method):
            print "Do something I want!"
            return True

        self.client.get(reverse(view))

What am I doing wrong?

kkurian
  • 3,844
  • 3
  • 30
  • 49
Kit Sunde
  • 35,972
  • 25
  • 125
  • 179
  • ```bar``` is in fact an "instance method," and its only parameter is ```self```. To be a class method, it would need to be parameterized with ```cls```, and would be callable as ```Promotion.foo()```. – cavaunpeu Sep 10 '15 at 19:57
  • 1
    Patchable object needs to be quoted like this: `@patch.object('my_app.models.FooClass', 'bar')` – Lasma Aug 29 '16 at 22:26
  • 2
    @cavaunpeu - not (just) parameterized with `cls`, but more importantly (since `self` and `cls` don't mean anything special in python), decorated with `@classmethod` – dwanderson Jan 23 '18 at 21:28

3 Answers3

94

To add onto Kit's answer, specifying a 3rd argument to patch.object() allows the mocked object/method to be specified. Otherwise, a default MagicMock object is used.

    def fake_bar(self):
        print "Do something I want!"
        return True

    @patch.object(my_app.models.FooClass, 'bar', fake_bar)
    def test_enter_promotion(self):
        self.client.get(reverse(view))
        # Do something I want!

Note that, if you specify the mocking object, then the default MagicMock() is no longer passed into the patched object -- e.g. no longer:

def test_enter_promotion(self, mock_method):

but instead:

def test_enter_promotion(self):

https://docs.python.org/3/library/unittest.mock.html#patch-object

bravmi
  • 505
  • 2
  • 7
  • 13
storm_m2138
  • 2,281
  • 2
  • 20
  • 18
44

Ah I was confused on where to apply that patch decorator. Fixed:

class ViewsDoSomething(TestCase):
    view = 'my_app.views.do_something'

    @patch.object(my_app.models.FooClass, 'bar')
    def test_enter_promotion(self, mock_method):
        self.client.get(reverse(view))
Kit Sunde
  • 35,972
  • 25
  • 125
  • 179
  • 26
    Where do you make the connection between the method to mock and the fake implementation now? – physicalattraction Sep 05 '14 at 08:34
  • @physicalattraction, the connection is made by the argument `mock_method` passed to the test function. I have been able to use this technique in one of my tests. This is useful when you only want to verify if the mocked method was called. – Kalyan Vedala Aug 10 '18 at 15:36
  • @rcode74: How to patch a method of an (some other object) instance inside the testing method. eg: def my_method_to_be_tested(...): r = some_script.some_class(...); r.how_to_patch_this_method. – Mr. Unnormalized Posterior Sep 10 '18 at 17:33
  • 1
    @imsrgadich, you would do something like r.how_to_patch_this_method = MagicMock(). You can check MagicMock documentation to see how to assign behavior to the mock object. – Kalyan Vedala Sep 10 '18 at 19:20
6

If you'd like to do assert_called etc. against the mocked method, use the patch.object and wrap replacement method in a MagicMock(side_effect=), ie:

with patch.object(class_to_mock, attribute_name, \
 MagicMock(side_effect=replacement_method)) as replacement_method_mock:

eg.:

from unittest.mock import patch, MagicMock

def fake_bar(self):
    print "Do something I want!"
    return True

def test_enter_promotion(self):
    with patch.object(my_app.models.FooClass, 'bar', MagicMock(side_effect=fake_bar)) as fake_bar_mock:
        self.client.get(reverse(view))
        # Do something I want!
        fake_bar_mock.assert_called()
Voy
  • 5,286
  • 1
  • 49
  • 59