[TIP] use mock to test a call with a side_effect that modifies a mutable arg?

Michael Foord michael at voidspace.org.uk
Wed Dec 28 09:02:15 PST 2011


On 28 Dec 2011, at 13:27, Michael Foord wrote:

> 
> On 28 Dec 2011, at 01:49, Gregory P. Smith wrote:
> 
>> I'm used to using pymox for most of my mocking but figured I'd give mock a try on some new code.
>> 
>> One thing I just ran into:
>> 
>> mything = Mock(spec=other_function_to_stub_out)
>> ... replace other_function_to_stub_out with mything ...
>> mything.return_value = (2, 3)
>> def _set_deps(mydict): mydict['deps'] = ['TEST']
>> mything.side_effect = _set_deps
>> ThingBeingTested(copy.deepcopy(example_input))
>> self.assertEqual(mything.call_count, 1)
>> self.assertEqual(mything.call_args, example_input)
>> 
>> The last assert raises error when example_input does not already contain the modification made my the side_effect.
>> 
>> The mything.side_effect modifies the dictionary argument passed to mything when it was called, that makes sense.  But I was hoping that call_args would be a snapshot of the args at call time (a copy.deepcopy of it) rather than having had the side_effect applied to it.
>> 
>> Is this intentional?
>> 
> 
> 
> Mock doesn't deepcopy arguments because deepcopy is slow and fragile, and it would break identity based equality / assertions.
> 
> Because mock stores the original arguments for assertions *later* it does have a problem with mutable arguments that are mutated between the call and the assertion. This is something that has never been a problem for *me* (which is why I haven't been particularly motivated to do anything about it), but I did have one user ask about it - so I added an example to the docs of ways to deal with it:
> 
> 	http://www.voidspace.org.uk/python/mock/examples.html#coping-with-mutable-arguments
> 
> As you surmise, one approach is to use side_effect to do the assert *at call time* as Mox would. (Note that side_effect must return DEFAULT in order for any return_value you have set to be used. Alternatively side_effect can just return mock.return_value directly.)
> 
> Another approach (easier to generalise) is to have side_effect deepcopy the arguments and delegate the call (with copied arguments) to a different mock that you actually do the assertions on.
> 
> A third approach would be a DeepCopyingMock that overrides __call__ and copies all arguments before delegating to Mock.__call__. If you create a subclass of Mock (or MagicMock) then all dynamically created attributes and the return_value will also use the subclass.
> 


And for completeness here's an implementation of CopyingMock (I'll add this to the example in the docs and consider including it in a future version of Mock):

>>> from copy import deepcopy
>>> from mock import MagicMock
>>> class CopyingMock(MagicMock):
...   def __call__(self, *args, **kwargs):
...     args = deepcopy(args)
...     kwargs = deepcopy(kwargs)
...     return super(CopyingMock, self).__call__(*args, **kwargs)
... 
>>> c = CopyingMock(return_value=None)
>>> c.foo
<CopyingMock name='mock.foo' id='4300369040'>
>>> c(a)
>>> a.add(1)
>>> c.assert_called_with(set())
>>> c.assert_called_with(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/compile/mock/mock.py", line 857, in assert_called_with
    raise AssertionError(msg)
AssertionError: Expected call: mock(set([1]))
Actual call: mock(set([]))
>>> 

All the best,

Michael Foord



> HTH,
> 
> Michael Foord
> 
> 
> 
>> I can update my side_effect function to include an assert on mydict instead.  But it wasn't the behavior I expected even though I can see why it probably happens even without digging into the mock sources.
>> 
>> In a pymox world this would've been written similar to:
>> 
>> mything(example_input).AndReturn((2, 3)
>> self.mox.ReplayAll()
>> ThingBeingTested(copy.deepcopy(example_input))
>> self.mox.VerifyAll()
>> 
>> -gps
>> _______________________________________________
>> testing-in-python mailing list
>> testing-in-python at lists.idyll.org
>> http://lists.idyll.org/listinfo/testing-in-python
> 
> 
> --
> http://www.voidspace.org.uk/
> 
> 
> May you do good and not evil
> May you find forgiveness for yourself and forgive others
> May you share freely, never taking more than you give.
> -- the sqlite blessing 
> http://www.sqlite.org/different.html
> 
> 
> 
> 
> 
> 
> _______________________________________________
> testing-in-python mailing list
> testing-in-python at lists.idyll.org
> http://lists.idyll.org/listinfo/testing-in-python
> 


--
http://www.voidspace.org.uk/


May you do good and not evil
May you find forgiveness for yourself and forgive others
May you share freely, never taking more than you give.
-- the sqlite blessing 
http://www.sqlite.org/different.html








More information about the testing-in-python mailing list