Mock objects

When writing tests, this regularly occurs: you are testing not only your own code but also the interaction with external resources, such as hardware, databases, web hosts, servers, and others. Some of these can be run safely, but certain tests are too slow, too dangerous, or even impossible to run. In those cases, mock objects are your friends; they can be used to fake anything, so you can be certain that your code still returns the expected results without having any variation from external factors.

Using unittest.mock

The unittest.mock library provides two base objects, Mock and MagicMock, to easily mock any external resources. The Mock object is just a general generic mock object and MagicMock is mostly the same, but it has all the magic methods such as __contains__ and __len__ defined. In addition to this, it can make your life even easier. This is because in addition to creating mock objects manually, it is possible to patch objects directly using the patch decorator/context manager.

The following function uses random to return True or False given governed by a certain probability distribution. Due to the random nature of a function like this, it is notoriously difficult to test, but not with unittest.mock. With the use of unittest.mock, it's easy to get repeatable results:

from unittest import mock
import random


def bernoulli(p):
    return random.random() > p


@mock.patch('random.random')
def test_bernoulli(mock_random):
    # Test for random value of 0.1
    mock_random.return_value = 0.1
    assert bernoulli(0.0)
    assert not bernoulli(0.1)
    assert mock_random.call_count == 2

Wonderful, isn't it? Without having to modify the original code, we can make sure that random.random now returns 0.1 instead of some random number. For completeness, the version that uses a context manager is given here:

from unittest import mock
import random


def bernoulli(p):
    return random.random() > p


def test_bernoulli():
    with mock.patch('random.random') as mock_random:
        mock_random.return_value = 0.1
        assert bernoulli(0.0)
        assert not bernoulli(0.1)
        assert mock_random.call_count == 2

The possibilities with mock objects are nearly endless. They vary from raising exceptions on access to faking entire APIs and returning different results on multiple calls. For example, let's fake deleting a file:

import os
from unittest import mock


def delete_file(filename):
    while os.path.exists(filename):
        os.unlink(filename)


@mock.patch('os.path.exists', side_effect=(True, False, False))
@mock.patch('os.unlink')
def test_delete_file(mock_exists, mock_unlink):
    # First try:
    delete_file('some non-existing file')

    # Second try:
    delete_file('some non-existing file')

Quite a bit of magic in this example! The side_effect parameter tells mock to return those values in that sequence, making sure that the first call to os.path.exists returns True and the other two return False. The mock.patch without arguments simply returns a callable that does nothing.

Using py.test monkeypatch

The monkeypatch object in py.test is a fixture that allows mocking as well. While it may seem useless after seeing the possibilities with unittest.mock, in summary, it's not. Some of the functionality does overlap, but while unittest.mock focuses on controlling and recording the actions of an object, the monkeypatch fixture focuses on simple and temporary environmental changes. Some examples of these are given in the following list:

  • Setting and deleting attributes using monkeypatch.setattr and monkeypatch.delattr
  • Setting and deleting dictionary items using monkeypatch.setitem and monkeypatch.delitem
  • Setting and deleting environment variables using monkeypatch.setenv and monkeypatch.delenv
  • Inserting an extra path to sys.path before all others using monkeypatch.syspath_prepend
  • Changing the directory using monkeypatch.chdir

To undo all modifications, simply use monkeypatch.undo.

For example, let's say that for a certain test, we need to work from a different directory. With mock, your options would be to mock pretty much all file functions, including the os.path functions, and even in that case, you will probably forget about a few. So, it's definitely not useful in this case. Another option would be to put the entire test into a try…finally block and just do an os.chdir before and after the testing code. This is quite a good and safe solution, but it's a bit of extra work, so let's compare the two methods:

import os


def test_chdir_monkeypatch(monkeypatch):
    monkeypatch.chdir('/dev')
    assert os.getcwd() == '/dev'
    monkeypatch.chdir('/')
    assert os.getcwd() == '/'


def test_chdir():
    original_directory = os.getcwd()
    try:
        os.chdir('/dev')
        assert os.getcwd() == '/dev'
        os.chdir('/')
        assert os.getcwd() == '/'
    finally:
        os.chdir(original_directory)

They effectively do the same, but one needs four lines of code whereas the other needs eight. All of these can easily be worked around with a few extra lines of code, of course, but the simpler the code is, the fewer mistakes you can make and the more readable it is.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset