Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python mock method call arguments display the last state of a list

I have a function which takes a list as a parameter. The function is called multiple times and every time some of the list values are updated. The mock object I am using to capture the call arguments, always shows the latest values in the list for all call arguments. The following code shows the problem.

from mock import MagicMock

def multiple_calls_test():
    m = MagicMock()
    params = [0, 'some_fixed_value', 'some_fixed_value']
    for i in xrange(1,10):
        params[0] = i
        m(params)
    for args in m.call_args_list:
        print args[0][0]

multiple_calls_test()

And here is the output, Notice all calls have 9 as the first list element.

[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']

Is there a way to force the mock object to make a copy of list argument instead of holding the reference to the actual list? Or some other way of asserting the correct value for every method execution? Thanks.

like image 843
Babar Avatar asked Apr 23 '14 23:04

Babar


2 Answers

For python 3.8 the accepted solution did not work anymore for me.
However, there is a solution in the official python docs:
https://docs.python.org/3/library/unittest.mock-examples.html#coping-with-mutable-arguments
You have to scroll down a bit to find the following:

An alternative approach is to create a subclass of Mock or MagicMock that copies (using copy.deepcopy()) the arguments. Here’s an example implementation:

from copy import deepcopy
class CopyingMock(MagicMock):
    def __call__(self, /, *args, **kwargs):
        args = deepcopy(args)
        kwargs = deepcopy(kwargs)
        return super(CopyingMock, self).__call__(*args, **kwargs)

This worked for me for python 3.8.

like image 41
B3N3D1K7 Avatar answered Sep 21 '22 19:09

B3N3D1K7


Unfortunately, this looks to be a shortcoming of the mock library, and from looking at the code this doesn't look to be possible without patching the mock library itself. However, it looks like there is a fairly lightweight way to do this to get the effect you are looking for:

import copy
from mock import MagicMock


class CopyArgsMagicMock(MagicMock):
    """
    Overrides MagicMock so that we store copies of arguments passed into calls to the
    mock object, instead of storing references to the original argument objects.
    """

    def _mock_call(_mock_self, *args, **kwargs):
        args_copy = copy.deepcopy(args)
        kwargs_copy = copy.deepcopy(kwargs)
        return super(CopyArgsMagicMock, self)._mock_call(*args_copy, **kwargs_copy)

Then (to state the obvious) simply replace your MagicMock with a CopyArgsMagicMock and you should see the required behavior.

Please note that this has only been tested for the use case provided, so this may not be a complete and robust solution to the problem, but hopefully it proves useful.

like image 144
robjohncox Avatar answered Sep 24 '22 19:09

robjohncox