Skip to content

Mock - dummy objects

What are mocks?

Mock is a replacement for an object that looks the same as a real object, but we control its behavior, e.g. define what values functions should return.

What are mocks for?

  • simulating a connection to a database,
  • simulating with resources that may be unavailable, e.g. connections to network services,
  • simulating objects that have not yet been created,
  • simulating to increase the speed of tests,

Example 1

Suppose we have such a class:

class BlogPostHistory:

    FILENAME = 'posts.txt'
    SEPARATOR = ','

    def __init__(self, title, desc):
        self._title = title
        self._desc = desc

    def save(self):
        with open(self.FILENAME, 'a+') as f:
            data = f'{self._title}{self.SEPARATOR}{self._desc}\n'
            f.write(data)

    def change_title(self, title):
        self._title = title
        try:
            self.save()
        except OSError:
            raise Exception("BlogPostHistory can't save history")

    def change_description(self, desc):
        self._desc = desc
        try:
            self.save()
        except OSError:
            raise Exception("BlogPostHistory can't save history")

    def get_properties(self):
        return self._title, self._desc

It is responsible for saving changes to blog posts. Yes, so that you can go back during editing, for example.

The problem that arises is how to test the * change_title * and * change_description * methods without saving to a file? The tests are run very often and our file quickly becomes huge. We don't want that. So how?

Mocks are the answer.

The test could look like this:

from ex4 import BlogPostHistory
from unittest.mock import patch, MagicMock
from unittest import TestCase


class BlogPostHistoryTests(TestCase):

    def test_change_title(self):
        mock = MagicMock()

        with patch('ex4.BlogPostHistory.save', mock):
            post_history = BlogPostHistory('title', 'desc')
            post_history.change_title("title2")
            assert post_history.get_properties() == ("title2", "desc")

In this case, the "with" syntax specifies which block of mock code will apply. The patch function takes "what to replace" as its first argument and "to what" as its second argument. In this case, we replaced the * save * function from the ex4 file, from the BlogPostHistory class, with the previously created object - that is, the mock. In a block covered by "with", running change_title will replace the class field and will obviously execute the save method, but we replaced it with a mock that does nothing.

A similar test for changing the description might look like this:

    def test_change_desc(self):
        mock = MagicMock()

        with patch('ex4.BlogPostHistory.save', mock):
            post_history = BlogPostHistory('title', 'desc')
            post_history.change_description("desc2")
            assert post_history.get_properties() == ("title", "desc2")

Unfortunately, this is not the end of the use cases for this class. The save method may throw an exception. We catch it in change_title and change_description, but throw another one with a nice message. How to test it? How to get Mock to throw OSError?

We can do it using the side_effect parameter - it allows us to perform e.g. another function or just - throwing an exception.

It could look like this:

    def test_problem_with_file(self):
        mock = MagicMock(side_effect=OSError)

        with patch('ex4.BlogPostHistory.save', mock):
            post_history = BlogPostHistory('title', 'desc')
            with pytest.raises(Exception):
                post_history.change_description("desc2")

So we define that the side effect of the mock is to throw an OSError exception and with change_description we assume that we will get Exception.

Example 2

Now we have such a class:

class AvgCalculator:
    def __init__(self, filename):
        self.filename = filename

    def _get_content(self):
        with open(self.filename, 'r') as f:
            lines = f.readlines()
            return [line.split(',') for line in lines]

    def _ensure_casted_data(self):
        data = self._get_content()
        new_data = []
        for x in data:
            new_data.append([int(n) for n in x])

        return new_data

    def calculate(self):
        numbers = self._ensure_casted_data()
        return [sum(x) / len(x) for x in numbers]

    def get_data(self):
        return self._get_content()

It is used to calculate the average of the file. In the file we assume that I am sure there will be numbers (tested in a different part of the whole system). Besides counting the mean, the class can also return raw data.

We would like to test this class in action. More precisely, calculate and get_data - but here's a problem. The problem is that the class reads from the file. Now, if we want to write tests, we have to create these files? From the previous example, you know we don't.

But ... In the previous example, our mock did not return anything. Now the problematic method _get_content is already returning data. How to deal with this? The return_value parameter comes in handy.

The tests could look like this:

from unittest.mock import MagicMock, patch

from ex5 import AvgCalculator


def test_calculate_avg():
    data = [['1', '2', '3', '4', '5'], ['2', '4', '1', '12']]
    mock = MagicMock(return_value=data)

    with patch('ex5.AvgCalculator._get_content', mock):
        avg_calc = AvgCalculator('fake.txt')
        assert avg_calc.calculate() == [3.0, 4.75]


def test_get_raw_data():
    data = [['1', '2', '3', '4', '5'], ['2', '4', '1', '12']]
    mock = MagicMock(return_value=data)

    with patch('ex5.AvgCalculator._get_content', mock):
        avg_calc = AvgCalculator('fake.txt')
        assert avg_calc.get_data() == data

We are using the return_value parameter here, which allows us to set the mock action to return what we defined.