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

Michael Foord fuzzyman at voidspace.org.uk
Wed Dec 28 05:27:59 PST 2011


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.

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








More information about the testing-in-python mailing list