Search code examples
pythonunit-testingmocking

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.


Solution

  • 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.