Mocking#

Sometimes our programmes depend on external circumstances, but during unit testing we shouldn’t worry about whether they work. And only test our code. So there are ways to change the behaviour of functions called in the programs being tested, to make it convenient for us to do the testing.

Check more in the unittest.mock documentation page.

import unittest
from unittest import mock

Syntax#

There are several ways to perform mocking using the unittest library:

  • Defining a mock object: This creates an object for which you can specify custom behavior.

  • Patching existing functions or methods: This is often the most practical and widely used approach. There are several syntax options for patching:

    • Using a context manager (with): The mock applies only within the scope of the with block, allowing you to define test-specific behavior.

    • Using a decorator for a specific method: The decorated method will use the mock instead of the real object during execution.

    • Applying to an entire unittest.TestCase subclass: All calls to the patched object within the test case will be handled by the corresponding mock.


The following cell shows what Mock is. Object for which you can define what it must return when called.

mock = unittest.mock.Mock()
mock.return_value = "Hello mock"
mock()
'Hello mock'

The following code shows the idea of the patch. The behaviour of my_fun inside the with block is changed according to the return_value attribute.

def my_fun(a, b):
    return "Original output"

print(my_fun(10, 20))

with unittest.mock.patch("__main__.my_fun") as mock_object:
    print(type(mock_object))
    mock_object.return_value = "Hello patch"
    print(my_fun())
Original output
<class 'unittest.mock.MagicMock'>
Hello patch

Note: The object returned by the context manager (mock_object) is of the unittest.mock.MagicMock type. You interact with it like any standard mock object, as it simply “translates” the defined behavior into the patched object.

Mock object#

You can define the behaviour of any object you like. Just pass a Mock() instance instead.

Check more in the Mock object


The following cell defines a class that can perform optional operations on the list using the list_operation method.

class Example:
    def __init__(self, list_operation):
        self.list_operation = list_operation
    def call_fun(self, lst):
        return self.list_operation(lst)

Now suppose you want to write test to this class and check that list_operation has been called in the correct way.

Just create Mock() instance and pass it insead of the function that have to be specified for Example in constructor.

The following cell shows an example that checks the output of Example.call_fun and that everything passed correctly.

class TestExample(unittest.TestCase):
    def test_ok(self):
        
        # Creating mock and difining it's behaviour
        mock_list_operation = unittest.mock.Mock()
        mock_list_operation.return_value = "mocked result"

        example = Example(mock_list_operation)
        
        sample_list = [1, 2, 3]
        
        result = example.call_fun(sample_list)
        # Assert that the result is as expected
        self.assertEqual(result, "mocked result")
        
        # Assert that the mock was called with the correct arguments
        mock_list_operation.assert_called_once_with(sample_list)

    def test_fail(self):
        '''
        And to be sure that evertything
        works lets try example that fails
        '''
        mock_list_operation = unittest.mock.Mock()
        mock_list_operation.return_value = "mocked result"
        example = Example(mock_list_operation)
        example.call_fun([10,20,30])
        mock_list_operation.assert_called_with([3,2,1])

ans = unittest.main(argv=[''], verbosity=2, exit=False)
del TestExample
test_fail (__main__.TestExample)
And to be sure that evertything ... FAIL
test_ok (__main__.TestExample) ... ok

======================================================================
FAIL: test_fail (__main__.TestExample)
And to be sure that evertything
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_279798/3126998038.py", line 33, in test_fail
    mock_list_operation.assert_called_with([3,2,1])
  File "/usr/lib/python3.10/unittest/mock.py", line 929, in assert_called_with
    raise AssertionError(_error_message()) from cause
AssertionError: expected call not found.
Expected: mock([3, 2, 1])
Actual: mock([10, 20, 30])

----------------------------------------------------------------------
Ran 2 tests in 0.002s

FAILED (failures=1)

As expected, test_ok ran fine and test_fail failed.

Patch#

You can change the behavior of an existing function or method by patching it with unittest.mock.patch. You just need to specify the target, which refers to an object in Python. Find out more in the specific page.


The following cell defines a function that we’ll patch shortly. It’s just a regular function with an obvious output.

def some_function(a, b):
    return a + b

print(some_function(3, 2))
5

The following cell wraps the function with the unittest.mock.patch context, where the path to that function is specified:

with unittest.mock.patch("__main__.some_function") as patch:
    print(type(patch))
    patch.return_value = "hello from patch"
    print(some_function(3, 2))
<class 'unittest.mock.MagicMock'>
hello from patch

The target variable of the context manager - patch here - is of type unittest.mock.MagicMock and actually defines the behavior of the function specified as the target. Thus, calling some_function returns the value specified in the return_value attribute.

Call details#

You can call unittests.mock.Mock instances, it tracks all calls:

  • The unittest.mock._Call object that describes the call.

  • The last call is stored in the call_args attribute of the mock.

  • The call_count attribute keeps the count of calls.

For more check the:


The following cell creates mock and few calls for it.

mock = unittest.mock.Mock()
mock(10, 20, 30)
mock("hello")
<Mock name='mock()' id='131503525130464'>

Consider which information about actions with the mock object we can get.

The call_args file contains the values passed in the last call.

mock.call_args
call('hello')

mock_calls contains a list of objects describing all calls to the mocked object.

mock.mock_calls
[call(10, 20, 30), call('hello')]

There is a special attribute for number of calls of the mock - call_count.

mock.call_count
2

Raising errors#

If you have cases with handling errors in your program, to test such cases you may need a tool to raise these errors.

One particular case is testing logging for try/except blocks.

You can use mocking mechanisms in such cases - you can pass the error to unittest.mock.MagicMock.side_effect and it’ll be raised when the mock is called.


Here is example of the function that uses request.get. But, suppose, it can raise some errors and we need to handle them. The handling assumes that we will write the error information to the log. The log, for simplicity, is just a toy - just a list to which we can write something.

import requests
def request_user(user_id, toy_log):
    try:
        response = requests.get(f"https://im_not_exist/{user_id}")
        if response.ok:
            return response.text
    except ValueError:
        toy_log.append("ValueError")
    except ZeroDivisionError:
        toy_log.append("ZeroDivisionError")
    return "Fail!"

The function handles ValueError and ZeroDivisionError. To check that all is well, we need to raise one of these within the try block.

The following cell shows the test case that will do the planned thing. It uses mocked_get.side_effect = ValueError("") to raise ValueError so that the appropriate string should appear in toy_log.

import unittest
from unittest.mock import patch

class TestUnit(unittest.TestCase):
    def test_error_raised(self):
        toy_log = []
        with patch("__main__.requests.get") as mocked_get:
            mocked_get.side_effect = ValueError("")
            output = request_user(10, toy_log)
            # check if answer was "Fail"
            self.assertEqual(output, "Fail!")
            # check if "ValueError" was 
            # appened to the toy log
            self.assertEqual(toy_log, ["ValueError"])

ans = unittest.main(argv=[''], verbosity=2, exit=False)
del TestUnit
test_error_raised (__main__.TestUnit) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.002s

OK