2

My understanding is that autospec in its simplest form as used here will check the signature of the function which is being mocked against the presented arguments. Its purpose is to raise an error if they do not match. In the code below it seems to inject an additional argument - the object itself. Why does the use of the mock module's autospec lead to the unexpected behavior shown here? For this question I've created a simplified version in the module simplebutton. When it's run as the main module the line, "It's no joke," is printed.

#module simplebutton
import sys


class _Dialog2:
    def callback(self):
        print("It's no joke")


def main():
    dialog = _Dialog2()
    dialog.callback()


if __name__ == '__main__':
    sys.exit(main())

The test module test_simplebutton contains two tests both of which work. Both mock the callback function. The second test, however, includes autospec=True.

    @unittest.mock.patch('simplebutton._Dialog2.callback',
                         name='callback', autospec=True)

In this test the callback function which should be called with no arguments has to be called with an argument of dialog otherwise the test fails.

Edit: Everyone knows that you don't call a method by method(instance) but by instance.method(). That was my mistake. Here it needs to be instance1.method('instance2') where instance1 is the mock and instance2 is the object containing the mocked method. Thanks to Michele d'Amico for this.

        mock_callback.assert_called_once_with(dialog)    

The test suite follows:

#module test_simplebutton
import unittest
import unittest.mock

import simplebutton


class Test_Dialog(unittest.TestCase):

    @unittest.mock.patch('simplebutton._Dialog2.callback',
                         name='callback')
    def test_direct_call_to_callback_by_mocking_1(self, mock_callback):
        dialog = simplebutton._Dialog2()
        dialog.callback()
        mock_callback.assert_called_once_with()

    @unittest.mock.patch('simplebutton._Dialog2.callback',
                         name='callback', autospec=True)
    def test_direct_call_to_callback_by_mocking_2(self, mock_callback):
        dialog = simplebutton._Dialog2()
        dialog.callback()
        mock_callback.assert_called_once_with(dialog)
lemi57ssss
  • 1,287
  • 4
  • 17
  • 36

1 Answers1

4

By autospec=True patch replace a object (in your case method) by a mock with same original objects's signature. Moreover the resulting mock cannot be extended: try to access to attributes or methods that are not in the original definition (or in MagicMock()) will raise an exception.

In the first case (without autospec=True) you are patching a bound method by an unbounded one. When you invoked your patched method, mock_callback is called as a function and not as a bound method of dialog object.

When you use autospec=True in @patch decorator it replace the original one by a new bound method mock_callback: that is like all other bound method and will be call by self as first argument. In order to the example more clear I changed it to explain better the behavior of autospec=True patch's parameter.

import unittest
import unittest.mock

import simplebutton

class Test_Dialog(unittest.TestCase):

    @unittest.mock.patch('simplebutton._Dialog2.callback')
    def test_direct_call_to_callback_by_mocking_1(self, mock_callback):
        dialog = simplebutton._Dialog2()
        dialog.callback()
        mock_callback.assert_called_once_with()
        mock_callback.reset_mock()
        simplebutton._Dialog2.callback()
        mock_callback.assert_called_once_with()

    @unittest.mock.patch('simplebutton._Dialog2.callback', autospec=True)
    def test_direct_call_to_callback_by_mocking_2(self, mock_callback):
        dialog = simplebutton._Dialog2()
        dialog.callback()
        mock_callback.assert_called_once_with(dialog)
        self.assertRaises(Exception, simplebutton._Dialog2.callback)

        dialog2 = simplebutton._Dialog2()
        dialog.callback()
        dialog2.callback()
        mock_callback.assert_has_calls([unittest.mock.call(dialog), unittest.mock.call(dialog2)])

In the first test we make either an explicit call to _Dialog2.callback() as unbound method by simplebutton._Dialog2.callback() and the behavior is exactly the same of dialog.callback().

In the second one if we try to call it as unbound like the first test it will raise an exception. Moreover if we call the method from two different objects we will found two different call to the same mock and we can identify them.

I hope is clear now what happen and what you should expect when you use autospec=True parameter.

Michele d'Amico
  • 22,111
  • 8
  • 69
  • 76
  • I see that the presence of 'dialog' in my second test is not an error. If, instead of writing, "In this test the callback function which should be called with no arguments...," I had written, "In this test the callback method which should be called with no arguments other than self…," my error would have been obvious. Your discussion helped me see this so I'm marking it as an accepted answer. Incidentally, I'm using Python 3 which has dispensed with unbound methods. – lemi57ssss Jan 13 '15 at 12:57
  • 1
    @lemi57ssss I'm speaking of *unbound* and *bound* method just to mark the difference from methods of object (that carry the `self` reference and use it when are invoked) and *function* that doesn't have self reference. My example work either in Python3 and Python 2.7 and explain the same behavior. I apologize if what a wrote is not clear... english is not my forte. – Michele d'Amico Jan 13 '15 at 13:17