Source: Mock-тесты
← Renderedmarkdown source:
---
description: Как писать mock-тесты на python
tags: tests python
title: Mock-тесты
---
[unittest.mock](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock)
[[unittest]] mock предоставляет базовый `Mock` class. После создания объекта, можно ассертить методы и аттрибуты класса, а так-же возвращать значения и устанавливать атрибуты, если это нужно.
Дополнительно предоставляется `patch()` декоратор который позволяет мокать тестируемые модули и атрибуты уровня класса. `MagicMock` - сабкласс `Mock`, который предоставляет множество магических методов.
```python
from unittest.mock import MagicMock
thing = ProductionClass()
thing.method = MagicMock(return_value=3)
thing.method(3, 4, 5, key='value')
thing.method.assert_called_with(3, 4, 5, key='value')
```
```pyjon
mock = Mock(side_effect=KeyError('foo'))
mock()
>>> Traceback (most recent call last):
...
KeyError: 'foo'
```
```python
values = {'a': 1, 'b': 2, 'c': 3}
def side_effect(arg):
return values[arg]
mock.side_effect = side_effect
mock('a'), mock('b'), mock('c')
mock.side_effect = [5, 4, 3, 2, 1]
mock(), mock(), mock()
```
```python
from unittest.mock import patch
@patch('module.ClassName2')
@patch('module.ClassName1')
def test(MockClass1, MockClass2):
module.ClassName1()
module.ClassName2()
assert MockClass1 is module.ClassName1
assert MockClass2 is module.ClassName2
assert MockClass1.called
assert MockClass2.called
test()
```
```python
with patch.object(ProductionClass, 'method', return_value=None) as mock_method:
thing = ProductionClass()
thing.method(1, 2, 3)
mock_method.assert_called_once_with(1, 2, 3)
```
Оригинальное состояние мокируемого объекта будет возвращено, когда тест завершается
```python
foo = {'key': 'value'}
original = foo.copy()
with patch.dict(foo, {'newkey': 'newvalue'}, clear=True):
assert foo == {'newkey': 'newvalue'}
assert foo == original
```
MagicMock мокирует все маг.методы #python
```python
mock = MagicMock()
mock.__str__.return_value = 'foobarbaz'
str(mock)
>>> 'foobarbaz'
mock.__str__.assert_called_with()
```
Маг.методам можно присваивать функции
```python
mock = Mock()
mock.__str__ = Mock(return_value='wheeeeee')
str(mock)
>>> 'wheeeeee'
```
Autospeck позволяет автоматически создавать моки всех атрибутов и методов функции
```python
from unittest.mock import create_autospec
def function(a, b, c):
pass
mock_function = create_autospec(function, return_value='fishy')
mock_function(1, 2, 3)
'fishy'
mock_function.assert_called_once_with(1, 2, 3)
mock_function('wrong arguments')
"""Traceback (most recent call last):
...
TypeError: <lambda>() takes exactly 3 arguments (1 given)"""
```
## [MocK class](https://docs.python.org/3/library/unittest.mock.html#the-mock-class)
Mock создает вызываемый объект, который создает новые моки атрибутов, когда мы получаем к ним доступ. Атрибуты всегда возвращают одно и тоже значение моков.
```python
class unittest.mock.Mock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, unsafe=False, **kwargs)
```
- specx - список строк или другой объект (например класс или инстанс) - это спецификация мока
- spec_set - список вариантов мока
- side_effect - функция, которая будет вызывана, когда вызван мок
- return_value - значение, которое вернет мок
- см.остальное в доке
Доступны такие ассерты:
- `assert_called()` проверка того, что мок был вызван хотябы однажды
- `assert_called_once()` вызван строго дин раз
- `assert_called_with(*args, **kwargs)` вызван с аргументами
```python
mock = Mock()
mock.method(1, 2, 3, test='wow')
<Mock name='mock.method()' id='...'>
mock.method.assert_called_with(1, 2, 3, test='wow')
```
- `assert_called_once_with(*args, **kwargs)`
- `assert_any_call(*args, **kwargs)` был вызов с любым из аргументов
- `assert_has_calls(calls, any_order=False)` были вызовы в определенном порядке
```python
mock = Mock(return_value=None)
mock(1)
mock(2)
mock(3)
mock(4)
calls = [call(2), call(3)]
mock.assert_has_calls(calls)
calls = [call(4), call(2), call(3)]
mock.assert_has_calls(calls, any_order=True)
```
- `assert_not_called()`
Остальные методы, начиная с [reset_mock()](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.reset_mock) - различные методы, конфигурирующие моки.
Доступны [асинхронные моки](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.AsyncMock). [[asyncio]]
### [Вызов мока](https://docs.python.org/3/library/unittest.mock.html#calling)
Объект мока вызываемы и возвращает значение, указанное в return_value. Дефолтно мок возвращает новый мок-объект. Возвращаемый объект создается первый раз при первом ассерте, дальше возвращается все время одно и тоже.
Вызовы пишутся в атрибуты call_args and call_args_list. Если задан side_effect, он запускается после создание объекта мока, так что вызов все равно будет записан в атрибут.
```python
m = MagicMock(side_effect=IndexError)
m(1, 2, 3)
Traceback (most recent call last):
...
IndexError
m.mock_calls
[call(1, 2, 3)]
m.side_effect = KeyError('Bang!')
m('two', 'three', 'four')
Traceback (most recent call last):
...
KeyError: 'Bang!'
m.mock_calls
[call(1, 2, 3), call('two', 'three', 'four')]
```
Если использовать функцию в качестве объекта side_effect, то она принимает аргументы вызова - это позволяет поднимать ошибку или возвращать значения в зависимости от атрибутов мока
```python
def side_effect(value):
return value + 1
m = MagicMock(side_effect=side_effect)
m(1)
2
m(2)
3
m.mock_calls
[call(1), call(2)]
```
Мы так-же в любой момент можем переопределить сайд эффект и возвращать дефолтно. Два способа как это сделать:
```python
m = MagicMock()
def side_effect(*args, **kwargs):
return m.return_value
m.side_effect = side_effect
m.return_value = 3
m()
3
def side_effect(*args, **kwargs):
return DEFAULT
m.side_effect = side_effect
m()
3
```
Чтобы возвращать дефолтное состояние, нужно установить side_effect в `None`
```python
m = MagicMock(return_value=6)
def side_effect(*args, **kwargs):
return 3
m.side_effect = side_effect
m()
3
m.side_effect = None
m()
6
```
Крмое того, side_effect можно итерирровать
```python
m = MagicMock(side_effect=[1, 2, 3])
m()
1
m()
2
m()
3
m()
Traceback (most recent call last):
...
StopIteration
```
И поднимать эксепшены в процессе итерации
```python
iterable = (33, ValueError, 66)
m = MagicMock(side_effect=iterable)
m()
33
m()
Traceback (most recent call last):
...
ValueError
m()
66
```
### [Удаление атрибутов](https://docs.python.org/3/library/unittest.mock.html#deleting-attributes)
```python
mock = MagicMock()
hasattr(mock, 'm')
True
del mock.m
hasattr(mock, 'm')
False
del mock.f
mock.f
Traceback (most recent call last):
...
AttributeError: f
```
### [Название mocka (атрибут name)](https://docs.python.org/3/library/unittest.mock.html#mock-names-and-the-name-attribute)
### [Использование мока, как атрибута](https://docs.python.org/3/library/unittest.mock.html#attaching-mocks-as-attributes)
При присоединении мока, как атрибута к другому моку, он становится дочерним моком.
```python
parent = MagicMock()
child1 = MagicMock(return_value=None)
child2 = MagicMock(return_value=None)
parent.child1 = child1
parent.child2 = child2
child1(1)
child2(2)
parent.mock_calls
[call.child1(1), call.child2(2)]
```
Другие подробности в статье
## [The patchers](https://docs.python.org/3/library/unittest.mock.html#the-patchers)
Декораторы patch используются для исправления объектов только в рамках функции, которую они декорируют. Они автоматически обрабатывают распаковку, даже если возникают исключения. Все эти функции также могут использоваться в операторах with или в качестве декораторов классов.
```python
unittest.mock.patch(target, new=DEFAULT, spec=None, create=False, spec_set=None, autospec=None, new_callable=None, **kwargs)
```
Может использоваться как декоратор функции, класса или контекст-менеджер.
- target должен быть в формате `'package.module.ClassName'`, Target испортируется и специфицированный объект заменяется на новый
- spec и spec_set пеередается в MagicMock
- new_callable определяет какие новые объекты буду вызваны при создании нового объекта
Подробнее об остальном в статье
```python
>>> @patch('__main__.SomeClass')
... def function(normal_argument, mock_class):
... print(mock_class is SomeClass)
...
>>> function(None)
True
```
```python
>>> class Class:
... def method(self):
... pass
...
>>> with patch('__main__.Class') as MockClass:
... instance = MockClass.return_value
... instance.method.return_value = 'foo'
... assert Class() is instance
... assert Class().method() == 'foo'
```
Пачт [может получать различные объекты](https://docs.python.org/3/library/unittest.mock.html#patch-object)
```python
@patch.object(SomeClass, 'class_method')
def test(mock_method):
SomeClass.class_method(3)
mock_method.assert_called_with(3)
test()
```
```python
foo = {}
@patch.dict(foo, {'newkey': 'newvalue'})
def test():
assert foo == {'newkey': 'newvalue'}
test()
assert foo == {}
```
```python
with patch.multiple(settings, FIRST_PATCH='one', SECOND_PATCH='two'):
...
```
У патчей есть start() и stop() методы. Это упрощает setUp для теста
```python
patcher = patch('package.module.ClassName')
from package import module
original = module.ClassName
new_mock = patcher.start()
assert module.ClassName is not original
assert module.ClassName is new_mock
patcher.stop()
assert module.ClassName is original
assert module.ClassName is not new_mock
```
Пример с сетап-тирдаун
```python
class MyTest(unittest.TestCase):
def setUp(self):
self.patcher1 = patch('package.module.Class1')
self.patcher2 = patch('package.module.Class2')
self.MockClass1 = self.patcher1.start()
self.MockClass2 = self.patcher2.start()
def tearDown(self):
self.patcher1.stop()
self.patcher2.stop()
def test_something(self):
assert package.module.Class1 is self.MockClass1
assert package.module.Class2 is self.MockClass2
MyTest('test_something').run()
```
Важно! Если setup вызовет ошибку - tearDown'а уже не будет. Проблему помогает решить [unittest.TestCase.addCleanup()](https://docs.python.org/3/library/unittest.html#unittest.TestCase.addCleanup)
```python
class MyTest(unittest.TestCase):
def setUp(self):
patcher = patch('package.module.Class')
self.MockClass = patcher.start()
self.addCleanup(patcher.stop)
def test_something(self):
assert package.module.Class is self.MockClass
```
Еще доступно вот это: `patch.stopall()`
### [Test prefix](https://docs.python.org/3/library/unittest.mock.html#test-prefix)
Это позволяет передать через декоратор что-то только специфическим методам класса, если декорируется класс. Аналог [unittest.TestLoader](https://docs.python.org/3/library/unittest.html#unittest.TestLoader)
```python
>>> patch.TEST_PREFIX = 'foo'
>>> value = 3
>>>
>>> @patch('__main__.value', 'not three')
... class Thing:
... def foo_one(self):
... print(value)
... def foo_two(self):
... print(value)
...
>>>
>>> Thing().foo_one()
not three
>>> Thing().foo_two()
not three
>>> value
3
```
### [Декораторы patch можно стакать](https://docs.python.org/3/library/unittest.mock.html#nesting-patch-decorators)
```python
@patch.object(SomeClass, 'class_method')
@patch.object(SomeClass, 'static_method')
def test(mock1, mock2):
assert SomeClass.static_method is mock1
assert SomeClass.class_method is mock2
SomeClass.static_method('foo')
SomeClass.class_method('bar')
return mock1, mock2
mock1, mock2 = test()
mock1.assert_called_once_with('foo')
mock2.assert_called_once_with('bar')
```
Остальное смотир в доке
## [MagicMock](https://docs.python.org/3/library/unittest.mock.html#magicmock-and-magic-method-support)
Позволяет мокироват ьмагические методы #python. Если для мока используется функция, ей обязательно необходимо передать self
```python
def __str__(self):
return 'fooble'
mock = Mock()
mock.__str__ = __str__
str(mock)
mock = Mock()
mock.__str__ = Mock()
mock.__str__.return_value = 'fooble'
str(mock)
'fooble'
mock = Mock()
mock.__iter__ = Mock(return_value=iter([]))
list(mock)
[]
```
Вот так можно замокать контекст-менеджер:
```python
mock = Mock()
mock.__enter__ = Mock(return_value='foo')
mock.__exit__ = Mock(return_value=False)
with mock as m:
assert m == 'foo'
mock.__enter__.assert_called_with()
mock.__exit__.assert_called_with(None, None, None)
```
Полный список того, что можно замокать:
- \__hash__, \__sizeof__, \__repr__ and \__str__
- \__dir__, \__format__ and \__subclasses__
- \__round__, \__floor__, \__trunc__ and \__ceil__
- Comparisons: \__lt__, \__gt__, \__le__, \__ge__, \__eq__ and \__ne__
- Container methods: \__getitem__, \__setitem__, \__delitem__, \__contains__, \__len__, \__iter__, \__reversed__ and \__missing__
- Context manager: \__enter__, \__exit__, \__aenter__ and \__aexit__
- Unary numeric methods: \__neg__, \__pos__ and \__invert__
- The numeric methods (including right hand and in-place variants): \__add__, \__sub__, \__mul__, \__matmul__, \__div__, \__truediv__, \__floordiv__, \__mod__, \__divmod__, \__lshift__, \__rshift__, \__and__, \__xor__, \__or__, and \__pow__
- Numeric conversion methods: \__complex__, \__int__, \__float__ and \__index__
- Descriptor methods: \__get__, \__set__ and \__delete__
- Pickling: \__reduce__, \__reduce_ex__, \__getinitargs__, \__getnewargs__, \__getstate__ and \__setstate__
- File system path representation: \__fspath__
- Asynchronous iteration methods: \__aiter__ and \__anext__
Данные методы не поддерживаются либо могут вызывать проблемы:
- \__getattr__, \__setattr__, \__init__ and \__new__
- \__prepare__, \__instancecheck__, \__subclasscheck__, \__del__
### [MagicMock](https://docs.python.org/3/library/unittest.mock.html#magic-mock)
Два варианта `MagicMock` and `NonCallableMagicMock`. Первый - это сабкласс от `Mock`, реализующий большинство меджик методов. Второй - его невызываемый собрат. Его конструктор аналогичен первому за исключением того, что return_value и side_effect не имеют значения.
```python
mock = MagicMock()
mock[3] = 'fish'
mock.__setitem__.assert_called_with(3, 'fish')
mock.__getitem__.return_value = 'result'
mock[2]
'result'
```
Доступные методы и их дефолтные значения
- \__lt__: NotImplemented
- \__gt__: NotImplemented
- \__le__: NotImplemented
- \__ge__: NotImplemented
- \__int__: 1
- \__contains__: False
- \__len__: 0
- \__iter__: iter([])
- \__exit__: False
- \__aexit__: False
- \__complex__: 1j
- \__float__: 1.0
- \__bool__: True
- \__index__: 1
- \__hash__: default hash for the mock
- \__str__: default str for the mock
- \__sizeof__: default sizeof for the mock
```python
mock = MagicMock()
int(mock)
1
len(mock)
0
list(mock)
[]
object() in mock
False
```
Подробнее читай доку - есть нюансы. Так-же ест ьметоды, котоыре поддерживаются, но не определены дефолтные значения.
## [Helpers](https://docs.python.org/3/library/unittest.mock.html#helpers)
`unittest.mock.sentinel` простйо способ создавать уникальные объекты для тестов
`unittest.mock.DEFAULT` заранее созданный сентинел. Как используется - смотри выше, там где обсуждался вывод дефолтного значения в side_effect
`unittest.mock.call(*args, **kwargs)` хелпер для упрощения ассершена
```python
m = MagicMock(return_value=None)
m(1, 2, a='foo', b='bar')
m()
m.call_args_list == [call(1, 2, a='foo', b='bar'), call()]
True
```
Реализованы методы call_args, call_args_list, mock_calls и method_calls. [Подробнее](https://docs.python.org/3/library/unittest.mock.html#call)
- `unittest.mock.create_autospec(spec, spec_set=False, instance=False, **kwargs)` позволяет создать мок, используя другой объект в качестве спецификации. [Подробный пример тут](https://docs.python.org/3/library/unittest.mock.html#auto-speccing)
- `unittest.mock.ANY` позволяет создать аргумент с "абсолютно любым значением"
```python
mock = Mock(return_value=None)
mock('foo', bar=object())
mock.assert_called_once_with('foo', bar=ANY)
```
```python
m = MagicMock(return_value=None)
m(1)
m(1, 2)
m(object())
m.mock_calls == [call(1), call(1, 2), ANY]
True
```
- `unittest.mock.FILTER_DIR`
- `unittest.mock.mock_open(mock=None, read_data=None)`
- `unittest.mock.seal(mock)`
## [Monkeypatching/mocking modules and environments](https://docs.pytest.org/en/6.2.x/monkeypatch.html)
Моки модулей и окружений можно делать в [[pytest]]. The `monkeypatch` fixture helps you to safely set/delete an attribute, dictionary item or environment variable, or to modify sys.path for importing
```python
monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.delattr(obj, name, raising=True)
monkeypatch.setitem(mapping, name, value)
monkeypatch.delitem(obj, name, raising=True)
monkeypatch.setenv(name, value, prepend=False)
monkeypatch.delenv(name, raising=True)
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)
```
Все модификации удаляются после того, как запрашиваемая функция в фикстуре выполняется. raising параметр определяет, что происходит при KeyError или AttributeError
Когда надо использовать?
- модифицировать методы API
- заменить значения словарей
- заменить переменные окружения
- заменить контекст, например текущие рабочие директории
- подменить `sys.path`
Простой пример
```python
# contents of test_module.py with source code and the test
from pathlib import Path
def getssh():
"""Simple function to return expanded homedir ssh path."""
return Path.home() / ".ssh"
def test_getssh(monkeypatch):
# mocked return function to replace Path.home
# always return '/abc'
def mockreturn():
return Path("/abc")
# Application of the monkeypatch to replace Path.home
# with the behavior of mockreturn defined above.
monkeypatch.setattr(Path, "home", mockreturn)
# Calling getssh() will use mockreturn in place of Path.home
# for this test with the monkeypatch.
x = getssh()
assert x == Path("/abc/.ssh")
```
Можно мокать возвращаемый объект, для этого надо [создать мок-класс](https://docs.pytest.org/en/6.2.x/monkeypatch.html#monkeypatching-returned-objects-building-mock-classes)
```python
# contents of app.py, a simple API retrieval example
import requests
def get_json(url):
"""Takes a URL, and returns the JSON."""
r = requests.get(url)
return r.json()
```
```python
# contents of test_app.py, a simple test for our API retrieval
# import requests for the purposes of monkeypatching
import requests
# our app.py that includes the get_json() function
# this is the previous code block example
import app
# custom class to be the mock return value
# will override the requests.Response returned from requests.get
class MockResponse:
# mock json() method always returns a specific testing dictionary
@staticmethod
def json():
return {"mock_key": "mock_response"}
def test_get_json(monkeypatch):
# Any arguments may be passed and mock_get() will always return our
# mocked object, which only has the .json() method.
def mock_get(*args, **kwargs):
return MockResponse()
# apply the monkeypatch for requests.get to mock_get
monkeypatch.setattr(requests, "get", mock_get)
# app.get_json, which contains requests.get, uses the monkeypatch
result = app.get_json("https://fakeurl")
assert result["mock_key"] == "mock_response"
```
Как мокать другие объекты - читай в документации
Либа [pytest-mock](https://github.com/pytest-dev/pytest-mock/) предоставляет доп.апи для моков в pytest, напирмер позволяет работать с файловой системой
```python
import os
class UnixFS:
@staticmethod
def rm(filename):
os.remove(filename)
def test_unix_fs(mocker):
mocker.patch('os.remove')
UnixFS.rm('file')
os.remove.assert_called_once_with('file')
```
Смотри еще:
- [[pytest]]
- [pytest-mock](https://github.com/pytest-dev/pytest-mock/)
[unittest]: unittest "Unittest"
[asyncio]: asyncio "Asyncio"
[pytest]: pytest "Pytest"