How to use mock and patch
This notes writes down how to create mock in python tests. There are two famous python mocking libraries:
- unittest.mock : python built-in mocking library.
- pytest-mock : inject mock.patch automatically into pytest fixture. (
recommended !
)
🧠 Tutorial
patch
is used to temporarily replace an object with a mock object in order to isolate and control the behavior of the code being tested. It allows you to replace a specific attribute or object with a mock object during the execution of a test.
mock
is a library that provides the ability to create and configure mock objects for testing it.
Creating Mocks
Type | Section |
---|---|
attribute | #Mock an Attribute |
function | #Mock an Function |
exception | #Mock an Exception |
context manager | #Mock a Context Manager |
Mock an Attribute
def test_mock_attribute(mocker):
mock = mocker.Mock()
mock.x = 3
assert mock.x == 3
Mock an Function
We can mocking a function by specify return_value
by Mock
class.
def test_mock(mocker):
mock = mocker.Mock()
mock.foo.return_value = 10
assert mock.foo() == 10
Mock()
and MagicMock()
?
MagicMock
is the subclass of Mock
. It contains all magic methods pre-created and read to use (e.g. __str__
or __len__
). Therefore, you should use MagicMock
when you need magic methods, and Mock
if you don't need them.
Mock an Exception
You can pass an exception by side_effect
def test_exception_by_side_effect(mocker):
mock = mocker.Mock()
mock.foo.side_effect = [10, 11, ValueError('something happend')]
assert mock.foo() == 10
assert mock.foo() == 11
with pytest.raises(ValueError) as exec_info:
mock.foo()
assert exec_info.message == 'something happend'
pytest.raises
context manager to assert exception
Mock a Context Manager
We can use __enter__
and __exit__
attributes of a mock object to mock a context manager
.
def test_context_manager(mocker):
mock = mocker.MagicMock()
mock.__enter__.return_value = 'mocked value'
with mock as value:
assert value == 'mocked value'
Side_effect v.s. return_value
side_effect
: Is used when you want a mocked function to have a specific side effect when it is called, rather than returning a specific value. It could be a function, an iterable of values.
return_value
: Is used to specify the value that a mocked function
( not a mocked property! ) should return when it is called.
mocking a static return value of a function
def test_return_value(mocker):
mock = mocker.Mock()
mock.foo.return_value = '123'
assert mock.foo() == '123'
assert mock.foo() == '123'
mocking a dynamic return value of a function
def test_side_effect(mocker):
mock = mocker.Mock()
mock.foo.side_effect = [10, 11, 12]
assert mock.foo() == 10
assert mock.foo() == 11
assert mock.foo() == 12
Or you can pass a callable by side_effect
def callable(*args, **kwargs):
name = kwargs['name']
return f'{name}-001'
def test_dynamic_side_effect(mocker):
mock = mocker.Mock()
mock.foo.side_effect = callable
assert mock.foo(name='foo') == 'foo-001'
Assert Calls
from unittest.mock import call, Mock
mock = Mock()
mock_obj(1, 2)
mock_obj('foo', 'bar')
calls = [call(1,2), call('foo', 'bar')]
mock.assert_has_calls(calls)
Spec v.s. spec_set
The spec
parameter is used to specify the class
or object
that the mock should be based on. It is used to restrict the mock object to only have the attributes and methods that are present in the specified class or object. Attempting to access an attribute not in the originating object will raise an AttributeError
.
def test_mock_spec(mocker):
mock = mocker.Mock(spec=3)
assert instance(mock, int)
class Foo:
def bar():
return 'bar'
def test_mock_spec_class(mocker)
mock = mocker.Mock(spec=Foo)
mock.foo() # raise an AttributeError !
mock.bar.return_value = 3
assert mock_bar() == 3
mock.x = 10
assertt mock.x == 10
While spec
also let you manually set the non-exist
attributes or methods but spec_set
forbids that and raise an AttributeError
.
class Foo:
def bar():
return 'bar'
def test_mock_spec_set_class(mocker):
mock = mocker.Mock(spec_set=Foo)
mock.x = 10 # raise an AtttributeError !
spec_set
should be your first choice.
Creating Patches
Patch a Class Method
class Foo:
def bar():
return 'bar'
def test_foo_bar(mocker):
mocker.patch.object(Foo, 'bar', return_value='mocked!')
foo = Foo()
assert foo.bar() == 'mocked'
Patch a Class Async Method
from unittest.mock import AsyncMock
class Foo
async def bar():
return 'bar'
@pytest.mark.async
async def test_async_foo_bar(mocker):
mock = mocker.patch.object(Foo, "bar", new_callable=AsyncMock)
mock.return_value = "mocked"
foo = Foo()
assert await foo.bar() == "mocked"