Mocking resources in unit tests is just as important and common as writing unit tests. However, a lot of people are not familiar with how to properly mock classes, objects or functions for tests, because the available documentation online is either too short or unnecessarily complicated. One of the main reasons for this confusion — several ways to do the same thing. Every other article out there seems to mock things in a different way. With this series of articles on mocking, I hope to bring some clarity on the topic.

Mocking with pytest

Pre-requisite

This is a tutorial on Mocking with pytest. I am operating with the assumption that you can write unit tests in Python using pytest.

Why Mock?

As you are here, reading this article, I will assume that you are familiar with mocking. In case you are not, let us do a quick overview of what it is and why we need it.

Say, you have a service that collects stock market data and gives information about the top gainers in a particular sector. You get the stock market information from a third party API, and process it to give out the results. Now, to test your code, you would not want to hit the API every time, as it will make the tests slower, and also the API provider would charge you for the extra hits. What you want here is a mock! A mock replaces a function with a dummy you can program to do whatever you choose. This is also called ‘Patching’. For the rest of this series, I am going to use ‘mock’ and ‘patch’ interchangeably.

Packages needed for Mocking

Unlike the majority of programming languages, Python comes with a built-in library for unit testing and mocking. They are powerful, self-sufficient and provide the functionality you need. The Pytest-mock plugin we will use, is a convenient wrapper around it which makes it easier to use it in combination with pytest.

If you look up articles on mocking, or if you read through the endless questions on Stackoverflow, you will frequently come across the words Mock, MagicMock, patch, etc. I'm going to demystify them here.

In Python, to mock, be it functions, objects or classes, you will mostly use Mock class. Mock class comes from the built-in unittest.mock module. From now on, anytime you come across Mock, know that it is from the unittest library. MagicMock is a subclass of Mock with some of the magic methods implemented. Magic methods are your usual dunder methods like__str__, __len__, etc.

For the most part, it does not matter which one you use, Mock or MagicMock. Unless you need magic methods like the above implemented, you can stick to Mock. Pytest-mock gives you access to both of these classes with an easy to use interface.

patch is another function that comes from the 'unittest' module that helps replace functions with mocks. Pytest mock has a wrapper for this too.

Installing Pytest Mock

Before you get started with using pytest-mock, you have to install it. You can install it with pip as follows:

pip install pytest-mock

This is a pytest plugin. So, it will also install pytest, if you have not installed it already.

Mocking a simple function

As this is the first article, we will keep it simple. We will start by mocking a simple function.

Say, we have a function get_operating_system that tells us whether we are using Windows or Linux.

# application.py 
from time import sleep  
def is_windows():    
    # This sleep could be some complex operation instead
    sleep(5)    
    return True  
def get_operating_system():    
    return 'Windows' if is_windows() else 'Linux'

This function uses another function is_windows to check if the current system is Windows or not. Assume that this is_windows function is quite complex taking several seconds to run. We can simulate this slow function by making the program sleep for 5 seconds every time it is called.

A pytest for get_operating_system() would be as follows:

# test_application.py

from application import get_operating_system

def test_get_operating_system():
    assert get_operating_system() == 'Windows'

Since, get_operating_system() calls a slower function is_windows, the test is going to be slow. This can be seen below in the output of running pytest which took 5.05 seconds.

$ pytest
================ test session starts ========================
Python 3.7.3, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /usr/Personal/Projects/pytest-and-mocking
plugins: mock-2.0.0
collected 1 item

test_application.py .                                    [100%]

================ 1 passed in 5.05s ==========================

Unit tests should be fast. We should be able to run hundreds of tests in seconds. A single test that takes five seconds slows down the test suite. Enter mocking, to makes our lives easier. If we patch the slow function, we can verify get_operating_system's behavior without waiting for five seconds.

Let’s mock this function with pytest-mock.

Pytest-mock provides a fixture called mocker. It provides a nice interface on top of python's built-in mocking constructs. You use mocker by passing it as an argument to your test function, and calling the mock and patch functions from it.

Say, you want the is_windows function to return True without taking those five precious seconds. We can patch it as follows:

mocker.patch('application.is_windows', return_value=True)

You have to refer to is_windows here as application.is_windows, given that it is the function in the application module. If we only patch is_windows, it will try to patch a function called is_windows in the 'test_application' file, which obviously does not exist. The format is always <module_name>.<function_name>. Knowing how to mock correctly is important and we will continue working on it in this series.

The updated test function with the patch is as follows:

# 'mocker' fixture provided by pytest-mock
def test_get_operating_system(mocker):  
    # Mock the slow function and return True always
    mocker.patch('application.is_windows', return_value=True) 
    assert get_operating_system() == 'Windows'

Now when you run the test, it will finish much faster.

$ pytest
============ test session starts ==================
Python 3.7.3, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /mnt/c/Personal/Projects/pytest-and-mocking
plugins: mock-2.0.0
collected 1 item

test_application.py .                          [100%]

=========== 1 passed in 0.11s ======================

As you can see, the test only 0.11 seconds. We have successfully patched the slow function and made the test suite faster.

Another advantage of mocking - you can make the mock function return anything. You can even make it raise errors to test how your code behaves in in those scenarios. We will see how all of this works and more, in the future articles.

For now, if you want to test the case where is_windows returnsFalse, write the following test:

def test_operation_system_is_linux(mocker):
    mocker.patch('application.is_windows', return_value=False) # set the return value to be False
    assert get_operating_system() == 'Linux'

Note that all of the mocks & patches set with mocker are function scoped i.e., they will only be available for that specific function. Therefore, you can patch the same function in multiple tests and they will not conflict with each other.

That is your first introduction to the world of mocking with pytest. We will cover more scenarios in the upcoming articles. Stay tuned, stay safe and stay awesome till then.

List of articles in this series:

Mocking Functions Part I 🢠 Current Article

Mocking Functions Part II

If you like this article, you can like this article to encourage me to put out the next article soon. If you think someone you know can benefit from this article, do share it with them.

If you want to thank me, you can say hi on twitter @durgaswaroop. And, if you want to support me here’s my paypal link: paypal.me/durgaswaroop

Attribution: Python Logo — https://www.python.org/community/logos/



Published

Category

Software

Tags