Python Testing: Mock Objects

0
160
12 min read

How to install Python Mocker

Python Mocker isn’t included in the standard Python distribution. That means that we need to download and install it.

Time for action – installing Python Mocker

  • At the time of this writing, Python Mocker’s home page is located at http://labix.org/mocker, while its downloads are hosted at https://launchpad.net/mocker/+download. Go ahead and download the newest version, and we’ll see about installing it.
  • The first thing that needs to be done is to unzip the downloaded file. It’s a .tar.bz2, which should just work for Unix, Linux, or OSX users. Windows users will need a third-party program (7-Zip works well: http://www.7-zip.org/) to uncompress the archive. Store the uncompressed file in some temporary location.
  • Once you have the files unzipped somewhere, go to that location via the command line. Now, to do this next step, you either need to be allowed to write files into your Python installation’s site-packages directory (which you are, if you’re the one who installed Python in the first place) or you need to be using Python version 2.6 or higher.
  • If you can write to site-packages, type
    $ python setup.py install
  • If you can’t write to site-packages, but you’re using Python 2.6 or higher, type
    $ python setup.py install --user

Sometimes, a tool called easy_install can simplify the installation process of Python modules and packages. If you want to give it a try, download and install setuptools from http://pypi.python.org/pypi/setuptools, according to the directions on that page, and then run the command easy_install mocker. Once that command is done, you should be ready to use Nose.

Once you have successfully run the installer, Python Mocker is ready for use.

What is a mock object in software testing?

“Mock” in this sense means “imitation,” and that’s exactly what a mock object does. Mock objects imitate the real objects that make up your program, without actually being those objects or relying on them in any way.

Instead of doing whatever the real object would do, a mock object performs predefined simple operations that look like what the real object should do. That means its methods return appropriate values (which you told it to return) or raise appropriate exceptions (which you told it to raise). A mock object is like a mockingbird; imitating the calls of other birds without comprehending them.

We’ve already used one mock object in our earlier work when we replaced time.time with an object (in Python, functions are objects) that returned an increasing series of numbers. The mock object was like time.time, except that it always returned the same series of numbers, no matter when we ran our test or how fast the computer was that we ran it on. In other words, it decoupled our test from an external variable.

That’s what mock objects are all about: decoupling tests from external variables. Sometimes those variables are things like the external time or processor speed, but usually the variables are the behavior of other units.

Python Mocker

The idea is pretty straightforward, but one look at that mock version of time.time shows that creating mock objects without using a toolkit of some sort can be a dense and annoying process, and can interfere with the readability of your tests. This is where Python Mocker (or any of several other mock object toolkits, depending on preference) comes in.

Time for action – exploring the basics of Mocker

We’ll walk through some of the simplest—and most useful—features of Mocker. To do that, we’ll write tests that describe a class representing a specific mathematical operation (multiplication) which can be applied to the values of arbitrary other mathematical operation objects. In other words, we’ll work on the guts of a spreadsheet program (or something similar).

We’re going to use Mocker to create mock objects to stand in place of the real operation objects.

  • Create up a text file to hold the tests, and add the following at the beginning (assuming that all the mathematical operations will be defined in a module called operations):
    >>> from mocker import Mocker
    >>> import operations
  • We’ve decided that every mathematical operation class should have a constructor accepting the objects representing the new object’s operands. It should also have an evaluate function that accepts a dictionary of variable bindings as its parameter and returns a number as the result. We can write the tests for the constructor fairly easily, so we do that first (Note that we’ve included some explanation in the test file, which is always a good idea):
    We're going to test out the constructor for the multiply operation, first.
    Since all that the constructor has to do is record all of the operands, 
    this is straightforward.

    >>> mocker = Mocker()
    >>> p1 = mocker.mock()
    >>> p2 = mocker.mock()
    >>> mocker.replay()
    >>> m = operations.multiply(p1, p2)
    >>> m.operands == (p1, p2)
    True
    >>> mocker.restore()
    >>> mocker.verify()

  • The tests for the evaluate method are somewhat more complicated, because there are several things we need to test. This is also where we start seeing the real advantages of Mocker:
    Now we're going to check the evaluate method for the multiply operation. 
    It should raise a ValueError if there are less than two operands, it should 
    call the evaluate methods of all operations that are operands of the multiply, 
    and of course it should return the correct value.

    >>> mocker = Mocker()
    >>> p1 = mocker.mock()
    >>> p1.evaluate({}) #doctest: +ELLIPSIS
    <mocker.Mock object at …>
    >>> mocker.result(97.43)

    >>> mocker.replay()

    >>> m = operations.multiply(p1)
    >>> m.evaluate({})
    Traceback (most recent call last):
    ValueError: multiply without at least two operands is meaningless

    >>> mocker.restore()
    >>> mocker.verify()

    >>> mocker = Mocker()
    >>> p1 = mocker.mock()
    >>> p1.evaluate({}) #doctest: +ELLIPSIS
    <mocker.Mock object at …>
    >>> mocker.result(97.43)
    >>> p2 = mocker.mock()
    >>> p2.evaluate({}) #doctest: +ELLIPSIS
    <mocker.Mock object at …>
    >>> mocker.result(-16.25)

    >>> mocker.replay()

    >>> m = operations.multiply(p1, p2)
    >>> round(m.evaluate({}), 2)
    -1583.24

    >>> mocker.restore()
    >>> mocker.verify()

  • If we run the tests now, we get a list of failed tests. Most of them are due to Mocker being unable to import the operations module, but the bottom of the list should look like this:

  • Finally, we’ll write some code in the operations module that passes these tests, producing the following:
    class multiply:
      def __init__(self, *operands):
        self.operands = operands
      def evaluate(self, bindings):
        vals = [x.evaluate(bindings) for x in self.operands]
      if len(vals) < 2:
        raise ValueError('multiply without at least two '
              'operands is meaningless')
      result = 1.0
      for val in vals:
         result *= val
      return result
  • Now when we run the tests, none of them should fail.

What just happened?

The difficulty in writing the tests for something like this comes(as it often does) from the need to decouple the multiplication class from all of the other mathematical operation classes, so that the results of the multiplication test only depend on whether multiplication works correctly.

We addressed this problem by using the Mocker framework for mock objects. The way Mocker works is that you first create an object representing the mocking context, by doing something such as mocker = Mocker(). The mocking context will help you create mock objects, and it will store information about how you expect them to be used. Additionally, it can help you temporarily replace library objects with mocks (like we’ve previously done with time.time) and restore the real objects to their places when you’re done. We’ll see more about doing that in a little while.

Once you have a mocking context, you create a mock object by calling its mock method, and then you demonstrate how you expect the mock objects to be used. The mocking context records your demonstration, so later on when you call its replay method it knows what usage to expect for each object and how it should respond. Your tests (which use the mock objects instead of the real objects that they imitate), go after the call to replay.

Finally, after test code has been run, you call the mocking context’s restore method to undo any replacements of library objects, and then verify to check that the actual usage of the mocks was as expected.

Our first use of Mocker was straightforward. We tested our constructor, which is specified to be extremely simple. It’s not supposed to do anything with its parameters, aside from store them away for later. Did we gain anything at all by using Mocker to create mock objects to use as the parameters, when the parameters aren’t even supposed to do anything? In fact, we did. Since we didn’t tell Mocker to expect any interactions with the mock objects, it will report nearly any usage of the parameters (storing them doesn’t count, because storing them isn’t actually interacting with them) as errors during the verify step. When we call mocker.verify(), Mocker looks back at how the parameters were really used and reports a failure if our constructor tried to perform some action on them. It’s another way to embed our expectations into our tests.

We used Mocker twice more, except in those later uses we told Mocker to expect a call to an evaluate method on the mock objects (i.e. p1 and p2), and to expect an empty dictionary as the parameter to each of the mock objects’ evaluate call. For each call we told it to expect, we also told it that its response should be to return a specific floating point number. Not coincidentally, that mimics the behavior of an operation object, and we can use the mocks in our tests of multiply.evaluate.

If multiply.evaluate hadn’t called the evaluate methods of mock, or if it had called one of them more than once, our mocker.verify call would have alerted us to the problem. This ability to describe not just what should be called but how often each thing should be called is a very useful too that makes our descriptions of what we expect much more complete. When multiply.evaluate calls the evaluate method of mock, the values that get returned are the ones that we specified, so we know exactly what multiply.evaluate ought to do. We can test it thoroughly, and we can do it without involving any of the other units of our code. Try changing how multiply.evaluate works and see what mocker.verify says about it.

Mocking functions

Normal objects (that is to say, objects with methods and attributes created by instantiating a class) aren’t the only things you can make mocks of. Functions are another kind of object that can be mocked, and it turns out to be pretty easy.

During your demonstration, if you want a mock object to represent a function, just call it. The mock object will recognize that you want it to behave like a function, and it will make a note of what parameters you passed it, so that it can compare them against what gets passed to it during the test.

For example, the following code creates a mock called func, which pretends to be a function that, when called once with the parameters 56 and hello, returns the number 11. The second part of the example uses the mock in a very simple test:

>>> from mocker import Mocker
>>> mocker = Mocker()
>>> func = mocker.mock()
>>> func(56, "hello") # doctest: +ELLIPSIS
<mocker.Mock object at ...>
>>> mocker.result(11)
>>> mocker.replay()
>>> func(56, "hello")
11
>>> mocker.restore()
>>> mocker.verify()

Mocking containers

Containers are another category of somewhat special objects that can be mocked. Like functions, containers can be mocked by simply using a mock object as if it were a container during your example.

Mock objects are able to understand examples that involve the following container operations: looking up a member, setting a member, deleting a member, finding the length, and getting an iterator over the members. Depending on the version of Mocker, membership testing via the in operator may also be available.

In the following example, all of the above capabilities are demonstrated, but the in tests are disabled for compatibility with versions of Mocker that don’t support them. Keep in mind that even though, after we call replay, the object called container looks like an actual container, it’s not. It’s just responding to stimuli we told it to expect, in the way we told it to respond. That’s why, when our test asks for an iterator, it returns None instead. That’s what we told it to do, and that’s all it knows.

>>> from mocker import Mocker
>>> mocker = Mocker()
>>> container = mocker.mock()
>>> container['hi'] = 18
>>> container['hi'] # doctest: +ELLIPSIS
<mocker.Mock object at ...>
>>> mocker.result(18)
>>> len(container)
0
>>> mocker.result(1)
>>> 'hi' in container # doctest: +SKIP
True
>>> mocker.result(True)
>>> iter(container) # doctest: +ELLIPSIS
<...>
>>> mocker.result(None)
>>> del container['hi']
>>> mocker.result(None)
>>> mocker.replay()
>>> container['hi'] = 18
>>> container['hi']
18
>>> len(container)
1
>>> 'hi' in container # doctest: +SKIP
True
>>> for key in container:
... print key
Traceback (most recent call last):
TypeError: iter() returned non-iterator of type 'NoneType'
>>> del container['hi']
>>> mocker.restore()
>>> mocker.verify()

Something to notice in the above example is that during the initial phase, a few of the demonstrations (for example, the call to len) did not return a mocker.Mock object, as we might have expected. For some operations, Python enforces that the result is of a particular type (for example, container lengths have to be integers), which forces Mocker to break its normal pattern. Instead of returning a generic mock object, it returns an object of the correct type, although the value of the returned object is meaningless. Fortunately, this only applies during the initial phase, when you’re showing Mocker what to expect, and only in a few cases, so it’s usually not a big deal. There are times when the returned mock objects are needed, though, so it’s worth knowing about the exceptions.


Subscribe to the weekly Packt Hub newsletter

* indicates required

LEAVE A REPLY

Please enter your comment!
Please enter your name here