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

configure_mock: improved mock configuration (take two)

emoticon:scanner A coupla days ago I blogged my ideas for making mock configuration less verbose. After some useful feedback in the comments here's an improved version.

from mock import Mock

def configure_mock(mock, **kwargs):
    if mock is None:
        mock = Mock()
    for arg, val in sorted(kwargs.items(),
                           key=lambda entry: len(entry[0].split('.'))):
        args = arg.split('.')
        final = args.pop()
        obj = mock
        for entry in args:
            obj = getattr(obj, entry)
        setattr(obj, final, val)
    return mock

And here is how you use it:

>>> args = {'foo.baz.return_value': 'fish', 'foo.side_effect':
... RuntimeError, 'side_effect': KeyError, 'foo.bar': 3}
...
>>> mock = configure_mock(None, **args)
>>> mock.foo.bar
3
>>> mock()
Traceback (most recent call last):
  ...
KeyError
>>> mock.foo.baz()
'fish'
>>> mock.foo()
Traceback (most recent call last):
  ...
RuntimeError

Standard dotted notation is used to specify return values, side effects and child attributes. Instead of normal keyword arguments you'll need to build a dictionary of arguments and pass them in with **. configure_mock creates a mock for you if you pass in None as the first argument.

This will probably be built-in to the Mock class and patchers in 0.8.0, but you can start using this convenience function right away.

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

Posted by Fuzzyman on 2011-03-24 00:34:06 | |

Categories: , , Tags: ,


Mock 0.7.0 final release and PyCon talk video

emoticon:movies Yay for conference driven development, I got the final release of mock 0.7.0 done in time for PyCon. No api changes since the release candidate. The only changes are documentation improvements (double yay!).

At PyCon I gave a talk on mock. It was an intro talk on how to use mock and why you should use it, but also covered some of the shiny new features in 0.7.0. The video of the talk is up (thanks to the PyCon video team):

My photos from PyCon, mainly of the language and vm summits and the sprints, are here:

Here's the release announcement for mock 0.7.0 final.

mock is a Python library for simple mocking and patching (replacing objects with mocks during test runs). The "headline features" in 0.7.0 are Python 3 support and the ability to mock magic methods. You can now mock objects that behave like containers or are used as context managers. mock is designed for use with unittest, based on the "action -> assertion" pattern rather than "record -> replay". People are happily using mock with Python test frameworks like nose and py.test. 0.7.0 is a major new release with a bunch of other new features and bugfixes as well.

The big change since 0.7.0 rc 1 is documentation changes including a stylish new Sphinx theme.

Three new pages particularly worth looking at are:

The full set of changes since 0.6.0 are:

http://www.voidspace.org.uk/python/mock/changelog.html#version-0-7-0

  • Python 3 compatibility
  • Ability to mock magic methods with Mock and addition of MagicMock with pre-created magic methods
  • Addition of mocksignature and mocksignature argument to patch and patch.object
  • Addition of patch.dict for changing dictionaries during a test
  • Ability to use patch, patch.object and patch.dict as class decorators
  • Renamed patch_object to patch.object (patch_object is deprecated)
  • Addition of soft comparisons: call_args, call_args_list and method_calls now return tuple-like objects which compare equal even when empty args or kwargs are skipped
  • patchers (patch, patch.object and patch.dict) have start and stop methods
  • Addition of assert_called_once_with method
  • Mocks can now be named (name argument to constructor) and the name is used in the repr
  • repr of a mock with a spec includes the class name of the spec
  • assert_called_with works with python -OO
  • New spec_set keyword argument to Mock and patch. If used, attempting to set an attribute on a mock not on the spec will raise an AttributeError
  • Mocks created with a spec can now pass isinstance tests (__class__ returns the type of the spec)
  • Added docstrings to all objects
  • Improved failure message for Mock.assert_called_with when the mock has not been called at all
  • Decorated functions / methods have their docstring and __module__ preserved on Python 2.4.
  • BUGFIX: mock.patch now works correctly with certain types of objects that proxy attribute access, like the django settings object
  • BUGFIX: mocks are now copyable (thanks to Ned Batchelder for reporting and diagnosing this)
  • BUGFIX: spec=True works with old style classes
  • BUGFIX: help(mock) works now (on the module). Can no longer use __bases__ as a valid sentinel name (thanks to Stephen Emslie for reporting and diagnosing this)
  • BUGFIX: side_effect now works with BaseException exceptions like KeyboardInterrupt
  • BUGFIX: reset_mock caused infinite recursion when a mock is set as its own return value
  • BUGFIX: patching the same object twice now restores the patches correctly
  • with statement tests now skipped on Python 2.4
  • Tests require unittest2 (or unittest2-py3k) to run
  • Tested with tox on Python 2.4 - 3.2, jython and pypy (excluding 3.0)
  • Added 'build_sphinx' command to setup.py (requires setuptools or distribute) Thanks to Florian Bauer
  • Switched from subversion to mercurial for source code control
  • Konrad Delong added as co-maintainer

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

Posted by Fuzzyman on 2011-03-22 00:47:46 | |

Categories: , Tags: , , , ,


Lococast PyCon Podcast

emoticon:acrobat Whilst I was at PyCon (which was awesome by the way) I recorded an interview with Rick Harding from the Lococast podcast. It's a half hour ramble around topics like IronPython, testing, PyCon, working for canonical, choice of operating system and other topics. It was good fun to chat to Rick and hopefully almost as fun to listen to:

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

Posted by Fuzzyman on 2011-03-22 00:27:23 | |

Categories: , , Tags:


Less verbose configuration of mock objects

emoticon:torch One of the things to come out of recent discussions with mock module users, and developers of alternative mock frameworks, is that for non-trivial mocking scenarios configuring your mocks can be verbose.

If you have a mock object, particularly one created for you by patch, setting up attributes and return values for methods takes one line for every aspect of configuration. This is nice and "declarative", but I've always seen "declarative" as just another way of saying verbose...

A feature I'm considering for mock 0.8.0 is an api for making configuring mocks less verbose. As is the way of these things, it is easy to prototype this first with a function that you can use right now.

configure_mock is a function that takes a Mock() instance along with keyword arguments for attributes of the mock you want to set. For example, to set mock.foo to 3 and mock.bar to None, you call:

>>> mock = Mock()
>>> configure_mock(mock, foo=3, bar=None)
>>> mock.foo
3
>>> print mock.bar
None

return_value and side_effect can be used to set them directly on the main mock anyway as they are just attributes.

>>> mock = Mock()
>>> configure_mock(mock, side_effect=KeyError)
>>> mock()
Traceback (most recent call last):
  ...
KeyError

This is fine for directly setting attributes, but what if you want to configure the return values or side effects of child mocks? How about using attribute__return_value and attribute__side_effect as a sort-of-dsl for specifying these in the same way?

>>> mock = Mock()
>>> configure_mock(mock, attribute__return_value=3, thing__side_effect=KeyError)
>>> mock.attribute()
3
>>> mock.thing()
Traceback (most recent call last):
  ...
KeyError

This could be extended to allow for extended dotted syntax from a dictionary, like setting return value on mock.foo.bar, or specifying attributes on mock.foo.return_value. If you have any opinions on this then please comment on the issue.

A minimal implementation of configure_mock that you can start using now is:

def configure_mock(mock, **kwargs):
    for arg, val in kwargs.items():
        if arg.endswith('__side_effect'):
            arg = arg[:-len('__side_effect')]
            getattr(mock, arg).side_effect = val
            continue

        if arg.endswith('__return_value'):
            arg = arg[:-len('__return_value')]
            getattr(mock, arg).return_value = val
            continue

        setattr(mock, arg, val)

Note

As suggested in the comments, a better way of specifying attribute return_value and side_effect would probably be to use "dotted notation". This would make the configure_mock call look like this:

attrs = {'foo.return_value': None, 'bar.side_effect': KeyError,
'spam.eggs': 3}
configure_mock(mock, **attrs)

This is likely to be the interface provided (to the mock contstructor and patchers) in 0.8.0.

This example will soon appear in the Further Examples page of the mock documentation, which is well worth a browse if you haven't seen it before (new in the documentation for 0.7.0).

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

Posted by Fuzzyman on 2011-03-21 18:40:40 | |

Categories: , Tags: ,


Mock: tracking order of calls and less verbose call assertions

emoticon:objects One advantage of jet-lag is opportunity to write blog entries. The Mock class in the mock module allows you to track the order of method calls on your mock objects through the method_calls attribute. This doesn't allow you to track the order of calls between separate mock objects, however we can use method_calls to achieve the same effect.

Because mocks track calls to child mocks in method_calls, and accessing an arbitrary attribute of a mock creates a child mock, we can create our separate mocks from a parent one. Calls to those child mock will then all be recorded, in order, in the method_calls of the parent:

>>> from mock import Mock
>>> manager = Mock()

>>> mock_foo = manager.foo
>>> mock_bar = manager.bar

>>> mock_foo.something()
<mock.Mock object at 0x5b93b0>
>>> mock_bar.different.thing()
<mock.Mock object at 0x5183f0>

>>> manager.method_calls
[('foo.something', (), {}), ('bar.different.thing', (), {})]

Using the "soft comparisons" feature of mock 0.7.0 we can make the final assertion about the expected calls less verbose:

>>> expected_calls = [('foo.something',), ('bar.different.thing',)]
>>> manager.method_calls == expected_calls
True

To make them even less verbose I would like to add a new call object to mock 0.8.0. You can see the issues I expect to work on for 0.8.0 in the issues list.

call would work something like this:

class Call(object):
    def __init__(self, name=None):
        self.name = name

    def __call__(self, *args, **kwargs):
        if self.name is None:
            return (args, kwargs)
        return (self.name, args, kwargs)

    def __getattr__(self, attr):
        if self.name is None:
            return Call(attr)
        name = '%s.%s' % (self.name, attr)
        return Call(name)

call = Call()

You can then use it like this:

>>> mock = Mock(return_value=None)
>>> mock(1, 2, 3)
>>> mock(a=3, b=6)
>>> mock.call_args_list == [call(1, 2, 3), call(a=3, b=6)]
True

>>> mock = Mock()
>>> mock.foo(1, 2 ,3)
<mock.Mock object at 0x58a5f0>
>>> mock.bar.baz(a=3, b=6)
<mock.Mock object at 0x58a730>
>>> mock.method_calls == [call.foo(1, 2, 3), call.bar.baz(a=3, b=6)]
True

And for good measure, the first example (tracking order of calls between mocks) using the new call object for assertions:

>>> manager = Mock()

>>> mock_foo = manager.foo
>>> mock_bar = manager.bar

>>> mock_foo.something()
<mock.Mock object at 0x5b93b0>
>>> mock_bar.different.thing()
<mock.Mock object at 0x5183f0>

>>> manager.method_calls == [call.foo.something(), call.bar.different.thing()]
True

This example will soon appear in the Further Examples page of the mock documentation, which is well worth a browse if you haven't seen it before (new in the documentation for 0.7.0).

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

Posted by Fuzzyman on 2011-03-21 18:39:21 | |

Categories: , Tags: ,


Hosted by Webfaction

Counter...