1

I am playing with patches and mocks and trying to understand the difference between patch of the method of a class and patch of the method of patched class. In both cases I create class instance and try to patch the bound method by bounded one with the explicit directive autospec=True. However, in first case I successfully patch the method with expected behaviour whereas in second case the patched object behaves unexpected.

It demonstrates the following code:

from mock import patch

class B(object):
    def zero(self):
        return 0

class A(object):
    def __init__(self):
        self.y = B()
        self.i = self.y.zero()

def mocked_zero(*args, **kwargs):
    return 1

a = A()
assert a.i == 0  # expected

with patch('__main__.B.zero', autospec=True) as mocked_B_zero:
    mocked_B_zero.side_effect = mocked_zero
    a = A()
    assert a.i == 1  # expected
    mocked_B_zero.assert_called_once_with(a.y)  # expected

with patch('__main__.B', autospec=True) as mocked_B:
    instance = mocked_B.return_value
    instance.zero.side_effect = mocked_zero

    # I placed the following line but it changes nothing
    instance.zero.autospec = True

    a = A()
    assert a.i == 1  # expected
    instance.zero.assert_called_once_with()  # why does it pass?

I expected the directive autospec=True will do the work and instance.zero.assert_called_once_with(a.y) will pass instead.

It should be noticed that directive autospec=True changes the mocked_b object.

with patch('__main__.B') as mocked_B:

creates an object mocked_B: <MagicMock name='B' id='...'>. Whereas

with patch('__main__.B', autospec=True) as mocked_B:

creates a different object mocked_B: <MagicMock name='B' spec='B' id='...'>

The is a great explanation of meaning of the keyword argument autospec=True in case of patching class method:

Mock's autospec injects a wrong argument into a called function

https://www.toptal.com/python/an-introduction-to-mocking-in-python (see comments)

but it remains unclear the meaning of the directive autospec=True in case of class patching.

So, I have several questions:

  1. What does directive autospec=True means in case of entire class patching?
  2. Why instance.zero.assert_called_once_with() passes in case of method patching of the patched class?
  3. Is it possible to patch a bound method by a bounded one in the second case so that instance.zero.assert_called_once_with(a.y) passes?

UPDATE

The situation where the need to patch both class and one of its method appears.

I need to test a class, say A, that uses in its implementation another class, say B. Simplified it looks like this:

class B(object):
    def __init__(self, config):
        pass

    def get_items(self):  # instead of 'zero' function
        while True:  # infinite loop
            item = self.request_to_db(some_params)
            yield item

class A(object):
    def __init__(self, config)
        self.b = B(config)

    def run(self):
         for item in self.b.get_items():
            do_some_action(item)

I need

  1. to check that instantiating the class A like a = A(config) the class B was called with parameter config.
  2. to check that calling a.run() the method get_items was called and is placed into the loop.

For this purpose I want

  1. to patch the entire class B with some mock object
  2. to patch the method get_items with some specially constructed mock object (side_effect option is used here) raising StopIteration exception after defined number of iterations.

Of course, do_some_action method is also patched.

Andriy
  • 1,270
  • 3
  • 17
  • 35
  • Can you clarify what you're trying to test in the second scenario? – Don Kirkby Oct 14 '17 at 19:18
  • I have extended my question explaining the situation where the need to patch both class and method appears. Hope, it is more clear now. Actually, I guess, I know how to reach the goal. But I don't understand why patching solely class method and patching the entire class and then patching class method yields the objects with different behaviour despite usage `autospec=True` option. – Andriy Oct 16 '17 at 20:19

0 Answers0