%load_ext ipython_pytest
What is Mocking?
pytest-mock
One of the mainstream mocking library is the standard one from unittest
, there are also pytest plugin pytest-mock
which wraps on unittest
.
%%pytest
def test_sum():
assert 1 == 1
============================= 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\tmpiih077gv
plugins: anyio-3.5.0, cov-3.0.0, mock-1.13.0
collected 1 item
_ipytesttmp.py . [100%]
============================== 1 passed in 0.06s ==============================
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.
mock.super_method(), mock.attribute_that_does_not_exist_at_all
(<Mock name='mock.super_method()' id='1587554283232'>,
<Mock name='mock.attribute_that_does_not_exist_at_all' id='1587554282512'>)
str(mock)
"<Mock id='1587554282848'>"
MagicMock
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'
= MagicMock() magic_mock
+ magic_mock magic_mock
<MagicMock name='mock.__add__()' id='1587563722784'>
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
%%pytest
import pandas as pd
def test_read_csv(mocker): # mocker is a special pytest fixture, so even though we haven't define it here but pytest understands it.
"pandas.read_csv", return_value = "fake_data")
mocker.patch(assert pd.read_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\tmpka9zv6ev
plugins: anyio-3.5.0, cov-3.0.0, mock-1.13.0
collected 1 item
_ipytesttmp.py . [100%]
============================== 1 passed in 0.09s ==============================
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
%%pytest
import pandas as pd
def test_read_csv(mocker): # mocker is a special pytest fixture, so even though we haven't define it here but pytest understands it.
"pandas.read_special_csv", return_value = "fake_data", create=False)
mocker.patch(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.
%%pytest
import pandas as pd
def test_read_csv(mocker): # mocker is a special pytest fixture, so even though we haven't define it here but pytest understands it.
"pandas.read_special_csv", return_value = "fake_data", create=True)
mocker.patch(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\tmphqbckliw
plugins: anyio-3.5.0, cov-3.0.0, mock-1.13.0
collected 1 item
_ipytesttmp.py . [100%]
============================== 1 passed in 0.10s ==============================
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
%%pytest -vvv
import pandas as pd
from unittest.mock import Mock
import pytest
def 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.
"pandas.read_csv", return_value = Mock(pd.DataFrame))
mocker.patch(= pd.read_csv("some_data")
df # A DataFrame method
df.mean()
def 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.
"pandas.read_csv", return_value = Mock(pd.DataFrame))
mocker.patch(= pd.read_csv("some_data")
df with pytest.raises(Exception):
df.not_a_dataframe_method()
============================= test session starts =============================
platform win32 -- Python 3.8.5, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- c:\users\lrcno\miniconda3\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\lrcno\AppData\Local\Temp\tmpyfiqtkoy
plugins: anyio-3.5.0, cov-3.0.0, mock-1.13.0
collecting ... collected 2 items
_ipytesttmp.py::test_read_csv_valid_method PASSED [ 50%]
_ipytesttmp.py::test_read_csv_invalid_method PASSED [100%]
============================== 2 passed in 0.16s ==============================