Python Mocker

About

That's what Mocker is about:

Recent changes

Please check the NEWS file.

Supported Python versions

The following Python versions are supported by the current version of Mocker:

Download

You can find released files at:

License

Mocker is available under the BSD license (have fun!).

Bug tracking

Performed in Launchpad:

Development

Development of Mocker may be tracked in Launchpad:

The source code may be obtained using Bazaar:

Code may be browsed at:

Author

Gustavo Niemeyer <gustavo@niemeyer.net>

Tutorial

Basis

A Mocker instance is used to command recording and replaying of expectations on any number of mock objects.

Expectations should be expressed for the mock object while in record mode (the initial one) by using the mock object itself, and using the mocker (and/or expect() as a helper) to define additional behavior for each expression. For instance:

   1 >>> mocker = Mocker()
   2 >>> obj = mocker.mock()
   3 
   4 >>> obj.hello()
   5 >>> mocker.result("Hi!")
   6 
   7 >>> mocker.replay()
   8 
   9 >>> obj.hello()
  10 'Hi!'
  11 
  12 >>> obj.bye()
  13 Traceback (most recent call last):
  14   ...
  15 mocker.MatchError: [Mocker] Unexpected expression: obj.bye
  16 
  17 >>> mocker.restore()
  18 >>> mocker.verify()

In this short excerpt a mock object is being created, then an expectation of a call to the hello() method was recorded, and when called the method should return the value "Hi!". Then, the mocker is put in replay mode, and the expectation is satisfied by calling the hello() method, which indeed returns "Hi!". Calling the unexpected method bye() on the mock raises an error. Finally, a call to the restore() method is performed to undo any needed changes made in the environment, and the verify() method is called to ensure that all defined expectations were met.

The same kind of logic may be expressed more elegantly using the with mocker: statement (Python 2.5+), as follows:

   1 >>> obj = mocker.mock()
   2 
   3 >>> obj.hello()
   4 >>> mocker.result("Hi!")
   5 
   6 >>> with mocker:
   7 ...     obj.hello()
   8 'Hi!'

Also, the MockerTestCase class, which integrates the mocker on a unittest.TestCase subclass, may be used to reduce the overhead of controlling the mocker. A test could be written as follows:

   1 >>> class SampleTest(MockerTestCase):
   2 ...
   3 ...     def test_hello(self):
   4 ...         obj = self.mocker.mock()
   5 ...         obj.hello()
   6 ...         self.mocker.result("Hi!")
   7 ...         self.mocker.replay()
   8 ...         self.assertEquals(obj.hello(), "Hi!")

After each test method is run, expectations defined will be verified, and any requested changes made to the environment will be restored.

Expression kinds

The following expression kinds are currently understood by Mocker, and will be properly recorded if used on a mock object:

Notice that these expressions may be nested. For instance, notice how the following expressions are recorded and replayed back:

   1 >>> obj = mocker.mock()
   2 >>> len(obj.attr.method("param"))
   3 >>> mocker.result(3)
   4 
   5 >>> mocker.replay()
   6 
   7 >>> method = obj.attr.method
   8 >>> result = method("param")
   9 >>> len(result)
  10 3

Expression reactions

Mocker may instruct mock objects to react in certain ways once recorded expressions are seen during replay mode. There are two different ways to specify that, and they are completely equivalent. One of them is using method calls on the mocker instance itself, and the other is using the expect() helper. As an example:

>>> obj1 = mocker.mock()
>>> obj1.hello()
>>> mocker.result("Hi!")

>>> obj2 = mocker.mock()
>>> expect(obj2.hello()).result("Hi!")

The following reactions are supported:

Check the API documentation for a more detailed explanation of these methods.

Parameter matching

Mocker offers a very flexible way to match parameters in method calls.

In simple cases, parameters are matched using basic equality. For instance:

   1 >>> obj = mocker.mock()
   2 
   3 >>> obj.hello("Joe")
   4 >>> mocker.result("Hi Joe!")
   5 
   6 >>> obj.hello("Jeff")
   7 >>> mocker.result("Hi Jeff!")

In more interesting cases, a few special parameters may be used to define the expectation.

   1 >>> obj = mocker.mock()
   2 
   3 >>> obj.hello("Joe", "morning")
   4 >>> mocker.result("Good morning, Joe!")
   5 
   6 >>> obj.hello("Jeff", ANY)
   7 >>> mocker.call(lambda name, when: "Good %s, Jeff!" % when)

It's possible to specify keyword arguments in a similar way, and also to match a variable number of positional or keyword arguments using the ARGS and KWARGS special parameters, respectively. These may be used alone or alongside other normal parameters.

As an example, the following code will record an expression which will match any method calls that take "Joe" as the second argument.

   1 >>> obj = mocker.mock()
   2 >>> obj.hello(ANY, "Joe", ARGS, KWARGS)

The following special parameters are available:

Patching limitations:

Ordering of expressions

By default, mocker won't force expressions to be expected precisely in the order they were recorded. You can change this behavior in a few different ways. Which one is used in a given occasion depends only on convenience.

One way to obtain ordering is calling mocker.order(). With this, the mocker will be put in a mode where any recorded expressions following it will only be met if they are replayed in the recorded order. When that's used, the mocker may be put back in unordered mode by calling mocker.unorder(), or by using a with mocker.order(): block, like so:

   1 >>> with mocker.order():
   2 ...     obj.hello()
   3 ...     mocker.result("Hi!")
   4 ...
   5 ...     obj.bye()
   6 ...     mocker.result("See ya!")
   7 
   8 >>> # mocker.unorder() -- Not needed when using a 'with' block.
   9 
  10 >>> mocker.replay()
  11 
  12 >>> obj.bye()
  13 Traceback (most recent call last):
  14   ...
  15 mocker.MatchError: [Mocker] Unexpected expression: obj.bye
  16 
  17 >>> obj.hello()
  18 'Hi!'
  19 >>> obj.bye()
  20 'See ya!'

Note how the first call to bye() raises an error, because hello() hadn't been called yet.

Proxying

Two powerful features of Mocker which aren't commonly seen in other mocking systems is the ability of proxying to existing objects, or even patching the real instance or class.

When an object is proxied, Mocker will create a mock object which will hold a reference to the real object, and will allow expressions to passthrough (mocked or not, and by default or on request).

To understand how it works in practice, let's define a new class:

   1 >>> class Greeting(object):
   2 ...
   3 ...     def hello(self, name):
   4 ...         return "Hi %s!" % name
   5 ...
   6 ...     def bye(self):
   7 ...         return "See ya!"

Let's see an example:

   1 >>> greeting = Greeting()
   2 
   3 >>> obj = mocker.proxy(greeting)
   4 >>> obj.hello("Jeff")
   5 >>> mocker.result("Hello buddy!")
   6 
   7 >>> obj.hello("Jim")
   8 >>> mocker.passthrough()
   9 
  10 >>> mocker.replay()
  11 
  12 >>> obj.hello("Jim")
  13 'Hi Jim!'
  14 
  15 >>> obj.hello("Jeff")
  16 'Hello buddy!'
  17 
  18 >>> obj.hello("Joe")
  19 'Hi Joe!'
  20 
  21 >>> obj.hello("Jim")
  22 Traceback (most recent call last):
  23   ...
  24 AssertionError: [Mocker] Unmet expectation:
  25 
  26 => obj.hello('Jim')
  27  - Performed more times than expected.

Note how we requested that one call taking "Jeff" as an argument should result in a message with Hello, and that a single call taking "Jim" as an argument should be made, and it should passthrough to the real object. Then, when replaying, we've used both of them, and then a third call which uses neither was made, and it passed through (that's the default setting, see the patch() method documentation for more details). Also interestingly, a second call with the "Jim" argument failed, because we said it'd happen only once (mocker.count() could have changed this).

Mocker also offers a proxy and replace mode, which basically means that if using the replace() method instead of proxy(), on replay time the returned mock object will replace the original object in namespaces of the whole Python interpreter (including modules, etc). Using that system, it's trivial to perform environment-level changes, such as in functions defined in standard Python modules. Both replace() and proxy() offer a mode where the passed object may be an "import string" instead of an object, to facilitate that kind of operation even more. Here is a simple example to illustrate how it may be used (luckily examples are still simple even when the explanation isn't):

   1 >>> obj = mocker.replace("time.time")
   2 >>> obj()
   3 >>> mocker.result(123)
   4 
   5 >>> mocker.replay()
   6 
   7 >>> from time import time
   8 >>> time()
   9 123

Note that this works even if modules have already run "from time import time" by the time mocker is instructed to perform the replacement.

As explained in the Basis section, changes will be undone when the mocker is restored.

Patching

Being able to patch objects is another powerful and uncommon feature found in Mocker, which is certainly handy in certain occasions.

When an object is patched, Mocker will return a mock object as usual, and will allow expectations to be defined on it. But then, when put on replay mode, Mocker will make modifications so that the real class or instance acts as defined by the recorded expectations. Just like with proxies, Mocker will allow expressions to passthrough to the real implementation when desired. Once the Mocker is restored (by calling mocker.restore() explicitly, or by the end of a with block, or by returning from a test method in MockerTestCase), Mocker will ensure that the patched object is put back in its original form.

Let's define the same Greeting class again, just for proximity with the following examples.

   1 >>> class Greeting(object):
   2 ...
   3 ...     def hello(self, name):
   4 ...         return "Hi %s!" % name

Patching works both in classes and in instances. When a class is patched, all instances of this class will act and be restricted by defined expectations, while if defined in the instance itself, only the given instance will reflect these.

Here is a simple example of changes performed for an instance:

   1 >>> greeting = Greeting()
   2 >>> obj = mocker.patch(greeting)
   3 
   4 >>> obj.hello("Jeff")
   5 <mocker.Mock object at 0xb76c608c>
   6 >>> mocker.result("Hello Jeff!")
   7 
   8 >>> mocker.replay()
   9 
  10 >>> greeting.hello("Jeff")
  11 'Hello Jeff!'
  12 >>> greeting.hello("Joe")
  13 'Hi Joe!'
  14 
  15 >>> type(greeting)
  16 <class '__main__.Greeting'>
  17 
  18 >>> mocker.restore()
  19 >>> greeting.hello("Jeff")
  20 'Hi Jeff!'

As explained in the Basis section, changes are fully undone when the mocker is restored.

Mocked expressions may easily passthrough to the real object, using the mocker.passthrough() method, as previously explained.

Specification checking for API conformance

One issue with using mock objects is that, depending on the amount of integration test performed, real objects may diverge from the mocked objects, and this might go unnoticed for a while. Mocker tries to minimize that situation by allowing method calls to be checked for conformance against the real object. These checks may be easily enabled for pure mock objects, and are performed by default with proxies and patched objects (it's possible to disable these; see the API documentation for more information).

Here is an example:

   1 >>> class Greeting(object):
   2 ...
   3 ...     def hello(self, name):
   4 ...         return "Hi %s!" % name
   5 
   6 
   7 >>> obj = mocker.mock(Greeting)
   8 >>> obj.hello("Joe", "what?")
   9 
  10 >>> mocker.replay()
  11 
  12 >>> obj.hello("Joe", "what?")
  13 Traceback (most recent call last):
  14   ...
  15 AssertionError: [Mocker] Unmet expectation:
  16 
  17 => obj.hello('Joe', 'what?')
  18  - Specification is hello(name): too many args provided

Notice that passing the spec as the first parameter of the mock() method will also enable the type simulation, as explained in the next section. To perform just specification checking, use the spec keyword argument explicitly.

To disable specification checking for a specific expression, Mocker offers the nospec() method. It's also possible to disable these checks on proxies and patched objects using spec=None when buildling these.

Type simulation

It's possible to ask Mocker to lie when its __class__ attribute is accessed. This makes it easier to perform type-related checks in the implementation while still using mock objects.

This is enabled by default on proxies, and may be easily requested with pure mock objects, as follows:

   1 >>> obj = mocker.mock(Greeting)
   2 
   3 >>> mocker.replay()
   4 
   5 >>> obj.__class__
   6 <class '__main__.Greeting'>
   7 
   8 >>> isinstance(obj, Greeting)
   9 True
  10 
  11 >>> type(obj)
  12 <class 'mocker.Mock'>

Note how type() will still reveal the truth. Also, the first argument to mock() will enable specification checking (see previous section). Use the type keyword argument if you really want just type simulation.

Extension to unittest.TestCase

Mocker offers MockerTestCase, which is a handy subclass of unittest.TestCase integrating Mocker support. It's by no means required for using what Mocker offers, but even then it's definitely a convenient integration.

Test methods will have a Mocker instance available on self.mocker, and at the end of each test method, expectations of the mocker will be verified, and any requested changes made to the environment will be restored, without any manual intervention.

It also offers self.expect, which is an alias for the expect() helper.

Tests which don't require the mocker will not be affected.

Here is a short example:

   1 >>> class SampleTest(MockerTestCase):
   2 ...
   3 ...    def test_hello(self):
   4 ...        obj = self.mocker.mock()
   5 ...        obj.hello()
   6 ...        self.mocker.result("Hi!")
   7 ...        self.mocker.replay()
   8 ...        self.assertEquals(obj.hello(), "Hi!")

In addition to the integration with Mocker, this class provides a few additional helper methods, which make it interesting even when Mocker itself isn't needed.

In addition to these, the following extensions to standard modules were made:

All of the expected negations and aliases for these methods are also available (note that one of them is spelled assertIsNot, rather than assertNotIs, for obvious reasons).

{i} Credits for the assertApproximates() idea go to Twisted's Trial.

mocker (last edited 2010-11-15 14:46:21 by GustavoNiemeyer)