Python Programming, news on the Voidspace Python Projects and all things techie.

mock 0.8 release candidate 1 and handling mutable arguments

emoticon:firefox I've released mock 0.8 release candidate 1. You can download it it or install it with:

pip install -U mock==dev

mock is a library for testing in Python. It allows you to replace parts of your system under test with mock objects. The latest stable release is 0.7.2, which you can download from pypi.

The main improvement in 0.8 rc1 is performance improvements, particularly in constructing MagiMock. Instantiating MagicMock is substantially faster than it was in 0.7 already, however patch creates a MagicMock by default now (in 0.7 patch creates a Mock by default). Instantiating MagicMock in 0.8 is still slower than instantiating a Mock in 0.7, but the difference is much less than it used to be.

There are also a couple of bugfixes. The full changelog for 0.8 rc1 is:

  • create_autospec on the return value of a mock class will use __call__ for the signature rather than __init__
  • Performance improvement instantiating Mock and MagicMock
  • Mocks used as magic methods have the same type as their parent instead of being hardcoded to MagicMock

The additional changes in 0.8 are explained in these blog entries:

Special thanks to Julian Berman for his help with diagnosing and improving performance in this release. I have started work on the doc changes and I'm hoping that the 0.8 final release won't be too far off.

Mock, and its now multifarious variants, are designed to "remember" arguments they are called with for making assertions about how they have been used. There are two ways it could do this: it could either keep references to the arguments or it could copy them.

Mock chooses to keep a reference to the arguments rather than copying them, mainly because this was easier to implement and met all my use cases at the time. As a consequence, if a mock is called with mutable arguments (like a set or a dictionary) that are changed (mutated) in between the call and the assertion then you have to make the assertion against the changed value rather than the value as it was at the call time. Here's an example of what I mean:

>>> from mock import MagicMock
>>> arg = set()
>>> m = MagicMock(return_value=None)
>>> m(arg)
>>> arg.add(1)
>>> m.assert_called_with(set())
Traceback (most recent call last):
  ...
AssertionError: Expected call: mock(set([]))
Actual call: mock(set([1]))

Even though m was called with an empty set, an assertion with an empty set fails.

The alternative would be to copy all arguments for every call (using copy.deepcopy). Unfortunately deepcopy is slow and fragile, and although it would solve this problem with mutable arguments it would break assertions that rely on object identity.

The mock documentation has an example showing two ways to solve this problem. There's a third way which is possibly more elegant, a subclass of MagicMock that deep-copies all arguments:

>>> 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)
...
>>> c = CopyingMock(return_value=None)
>>> arg = set()
>>> c(arg)
>>> arg.add(1)
>>> c.assert_called_with(set())
>>> c.assert_called_with(arg)
Traceback (most recent call last):
  ...
AssertionError: Expected call: mock(set([1]))
Actual call: mock(set([]))
>>> c.foo
<CopyingMock name='mock.foo' id='...'>

When you subclass Mock or MagicMock all dynamically created attributes, and the return_value will use your subclass automatically. That means all children of a CopyingMock will also have the type CopyingMock.

This goes nicely with the new-in-0.8 new_callable argument to patch that allows you to specify the mock class to use:

with patch('some.object', new_callable=CopyMock) as mock_thing:
        # do things...

Like this post? Digg it or Del.icio.us it.

Posted by Fuzzyman on 2011-12-29 13:04:47 | |

Categories: , Tags: , , , , ,


Hosted by Webfaction

Counter...