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.
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
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/