Mocking is important for a few reasons. * You want to have fast unittest (within second) * You don’t want to put loading or have any side-effect to your actual servers/database (e.g. mock writing to a database)
Mock and MagicMock
There are two main mock object you can used with the standard unittest library from unittest.mock.
from unittest.mock import Mock, MagicMock, patch
Mock
mock = Mock()
With the Mock object, you can treat it like a magic object that have any attributes or methods.
The “magic” comes from the magic methods of python object, for example, when you add two object together, it is calling the __add__ magic method under the hook.
mock + mock
TypeError: unsupported operand type(s) for +: 'Mock' and 'Mock'
With MagicMock, you get these magic methods for free, this is why adding two mock will not throw an error but adding two Mock will result in a TypeError
Let say we want to mock the pandas.read_csv function, because we don’t actually want it to read a data, but just return some mock data whenever it is called. It’s easier to explain with an example.
Mocking with real library
import pandas as pddef test_read_csv(mocker): # mocker is a special pytest fixture, so even though we haven't define it here but pytest understands it. mocker.patch("pandas.read_csv", return_value ="fake_data")assert pd.read_csv("some_data") =="fake_data"
In reality, you should get a Dataframe object, but here we mock the return value to return a str, and you can see the test actually pass.
mocker.patch with create=True
import pandas as pddef test_read_csv(mocker): # mocker is a special pytest fixture, so even though we haven't define it here but pytest understands it. mocker.patch("pandas.read_special_csv", return_value ="fake_data", create=False)assert pd.read_special_csv("some_data") =="fake_data"
============================= test session starts =============================
platform win32 -- Python 3.8.5, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: C:\Users\lrcno\AppData\Local\Temp\tmpzbddlxxg
plugins: anyio-3.5.0, cov-3.0.0, mock-1.13.0
collected 1 item
_ipytesttmp.py F [100%]
================================== FAILURES ===================================
________________________________ test_read_csv ________________________________
mocker = <pytest_mock.plugin.MockFixture object at 0x00000171B28B1820>
def test_read_csv(mocker): # mocker is a special pytest fixture, so even though we haven't define it here but pytest understands it.
> mocker.patch("pandas.read_special_csv", return_value = "fake_data", create=False)
_ipytesttmp.py:4:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
..\..\..\..\miniconda3\lib\site-packages\pytest_mock\plugin.py:193: in __call__
return self._start_patch(self.mock_module.patch, *args, **kwargs)
..\..\..\..\miniconda3\lib\site-packages\pytest_mock\plugin.py:157: in _start_patch
mocked = p.start()
..\..\..\..\miniconda3\lib\unittest\mock.py:1529: in start
result = self.__enter__()
..\..\..\..\miniconda3\lib\unittest\mock.py:1393: in __enter__
original, local = self.get_original()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <unittest.mock._patch object at 0x00000171B28B10D0>
def get_original(self):
target = self.getter()
name = self.attribute
original = DEFAULT
local = False
try:
original = target.__dict__[name]
except (AttributeError, KeyError):
original = getattr(target, name, DEFAULT)
else:
local = True
if name in _builtins and isinstance(target, ModuleType):
self.create = True
if not self.create and original is DEFAULT:
> raise AttributeError(
"%s does not have the attribute %r" % (target, name)
)
E AttributeError: <module 'pandas' from 'c:\\users\\lrcno\\miniconda3\\lib\\site-packages\\pandas\\__init__.py'> does not have the attribute 'read_special_csv'
..\..\..\..\miniconda3\lib\unittest\mock.py:1366: AttributeError
=========================== short test summary info ===========================
FAILED _ipytesttmp.py::test_read_csv - AttributeError: <module 'pandas' from ...
============================== 1 failed in 0.43s ==============================
Now we fail the test because pandas.read_special_csv does not exist. However, with create=True you can make the test pass again. Normally you won’t want to do this, but it is an option that available.
import pandas as pddef test_read_csv(mocker): # mocker is a special pytest fixture, so even though we haven't define it here but pytest understands it. mocker.patch("pandas.read_special_csv", return_value ="fake_data", create=True)assert pd.read_special_csv("some_data") =="fake_data"
More often, you would want your mock resemble your real object, which means it has the same attributes and method, but it should fails when the method being called isn’t valid. You may specify the return_value with the mock type
import pandas as pdfrom unittest.mock import Mockimport pytestdef test_read_csv_valid_method(mocker): # mocker is a special pytest fixture, so even though we haven't define it here but pytest understands it. mocker.patch("pandas.read_csv", return_value = Mock(pd.DataFrame)) df = pd.read_csv("some_data") df.mean() # A DataFrame methoddef test_read_csv_invalid_method(mocker): # mocker is a special pytest fixture, so even though we haven't define it here but pytest understands it. mocker.patch("pandas.read_csv", return_value = Mock(pd.DataFrame)) df = pd.read_csv("some_data")with pytest.raises(Exception): df.not_a_dataframe_method()