Документация Python неофициальный перевод
Содержание страницы

unittest.mock – начало работыunittest.mock – getting started

Добавлено в версии 3.3.

Использование MockUsing Mock

Методы подмены MockMock patching methods

Типичные случаи использования объектов Mock:

  • Подмена методов

  • Запись вызовов методов объектов

Возможно, потребуется заменить метод объекта, чтобы проверить, что другая часть системы вызывает его с правильными аргументами:

>>> real = SomeClass()
>>> real.method = MagicMock(name='method')
>>> real.method(3, 4, 5, key='value')
<MagicMock name='method()' id='...'>

После того как mock был использован (в данном примере real.method), он содержит методы и атрибуты, с помощью которых можно проверять, как именно он использовался.

Примечание

В большинстве этих примеров классы Mock и MagicMock взаимозаменяемы. Поскольку MagicMock – более функциональный класс, его имеет смысл использовать по умолчанию.

После вызова mock его атрибут called устанавливается в True. Что более важно, можно использовать метод assert_called_with() или assert_called_once_with(), чтобы проверить, что он был вызван с правильными аргументами.

Этот пример проверяет, что вызов ProductionClass().method приводит к вызову метода something:

>>> class ProductionClass:
...     def method(self):
...         self.something(1, 2, 3)
...     def something(self, a, b, c):
...         pass
...
>>> real = ProductionClass()
>>> real.something = MagicMock()
>>> real.method()
>>> real.something.assert_called_once_with(1, 2, 3)

Mock для вызовов методов объектаMock for method calls on an object

В последнем примере мы напрямую подменили метод объекта, чтобы проверить, что он был вызван корректно. Другой типичный случай – передать объект в метод (или какую-то часть тестируемой системы), а затем проверить, что он используется правильным образом.

Простой класс ProductionClass ниже имеет метод closer. Если его вызвать с объектом, то он вызовет close у этого объекта.

>>> class ProductionClass:
...     def closer(self, something):
...         something.close()
...

Итак, чтобы протестировать это, нужно передать объект с методом close и проверить, что он был вызван корректно.

>>> real = ProductionClass()
>>> mock = Mock()
>>> real.closer(mock)
>>> mock.close.assert_called_with()

Нам не нужно прилагать усилий, чтобы предоставить метод 'close' в нашем mock. Обращение к close создаёт его. Поэтому, если 'close' ещё не был вызван, то обращение к нему в тесте создаст его, но assert_called_with() вызовет исключение, указывающее на ошибку.

Макетирование классовMocking classes

Типичный сценарий – подменить классы, которые инстанциируются тестируемым кодом. При подмене класса он заменяется mock. Экземпляры создаются вызовом класса. Это значит, что «экземпляр mock» доступен через возвращаемое значение подменённого класса.

В примере ниже есть функция some_function, которая создаёт экземпляр Foo и вызывает его метод. Вызов patch() заменяет класс Foo на mock. Экземпляр Foo – это результат вызова mock, поэтому он настраивается путём изменения mock return_value.

>>> def some_function():
...     instance = module.Foo()
...     return instance.method()
...
>>> with patch('module.Foo') as mock:
...     instance = mock.return_value
...     instance.method.return_value = 'the result'
...     result = some_function()
...     assert result == 'the result'

Именование макетовNaming your mocks

Полезно давать вашим mock имена. Имя отображается в repr mock и может помочь, когда mock фигурирует в сообщениях об ошибках тестов. Имя также распространяется на атрибуты и методы mock:

>>> mock = MagicMock(name='foo')
>>> mock
<MagicMock name='foo' id='...'>
>>> mock.method
<MagicMock name='foo.method' id='...'>

Отслеживание всех вызововTracking all calls

Атрибут mock_calls записывает все вызовы дочерних атрибутов mock – а также их дочерних атрибутов.

>>> mock = MagicMock()
>>> mock.method()
<MagicMock name='mock.method()' id='...'>
>>> mock.attribute.method(10, x=53)
<MagicMock name='mock.attribute.method()' id='...'>
>>> mock.mock_calls
[call.method(), call.attribute.method(10, x=53)]

Если сделать проверку относительно mock_calls и были вызваны какие-либо непредвиденные методы, то проверка не пройдёт. Это полезно, поскольку помимо проверки того, что ожидаемые вызовы были сделаны, вы также проверяете, что они были сделаны в правильном порядке и без дополнительных вызовов:

Объект call используется для создания списков, которые затем сравниваются с mock_calls:

>>> expected = [call.method(), call.attribute.method(10, x=53)]
>>> mock.mock_calls == expected
True

Однако параметры вызовов, возвращающих макеты, не записываются, а значит, невозможно отслеживать вложенные вызовы, в которых важны параметры, использованные для создания родительских объектов:

>>> m = Mock()
>>> m.factory(important=True).deliver()
<Mock name='mock.factory().deliver()' id='...'>
>>> m.mock_calls[-1] == call.factory(important=False).deliver()
True

Установка возвращаемых значений и атрибутовSetting return values and attributes

Установка возвращаемых значений для mock-объекта очень проста:

>>> mock = Mock()
>>> mock.return_value = 3
>>> mock()
3

Конечно, то же самое можно сделать для методов mock:

>>> mock = Mock()
>>> mock.method.return_value = 3
>>> mock.method()
3

Возвращаемое значение также можно задать в конструкторе:

>>> mock = Mock(return_value=3)
>>> mock()
3

Если требуется задать атрибут на mock, сделайте это:

>>> mock = Mock()
>>> mock.x = 3
>>> mock.x
3

Иногда требуется имитировать более сложную ситуацию, например mock.connection.cursor().execute("SELECT 1"). Если нужно, чтобы этот вызов возвращал список, то придётся настроить результат вложенного вызова.

Можно использовать call, чтобы построить набор вызовов в виде «цепочки вызовов», как здесь, для удобной последующей проверки утверждений:

>>> mock = Mock()
>>> cursor = mock.connection.cursor.return_value
>>> cursor.execute.return_value = ['foo']
>>> mock.connection.cursor().execute("SELECT 1")
['foo']
>>> expected = call.connection.cursor().execute("SELECT 1").call_list()
>>> mock.mock_calls
[call.connection.cursor(), call.connection.cursor().execute('SELECT 1')]
>>> mock.mock_calls == expected
True

Именно вызов .call_list() превращает наш объект вызова в список вызовов, представляющих цепочку.

Генерация исключений с помощью моковRaising exceptions with mocks

Полезный атрибут – side_effect. Если присвоить ему класс исключения или экземпляр, то при вызове мока будет возбуждено это исключение.

>>> mock = Mock(side_effect=Exception('Boom!'))
>>> mock()
Traceback (most recent call last):
  ...
Exception: Boom!

Функции и итерируемые объекты в качестве побочного эффектаSide effect functions and iterables

side_effect также можно установить в функцию или итерируемый объект. Сценарий использования side_effect в качестве итерируемого объекта – когда мок будет вызываться несколько раз, и каждый вызов должен возвращать другое значение. Если side_effect присвоен итерируемый объект, каждый вызов мока возвращает следующий элемент из этого итерируемого объекта:

>>> mock = MagicMock(side_effect=[4, 5, 6])
>>> mock()
4
>>> mock()
5
>>> mock()
6

Для более продвинутых сценариев, например динамического изменения возвращаемых значений в зависимости от того, с чем вызывается мок, side_effect может быть функцией. Функция будет вызвана с теми же аргументами, что и мок. Что бы функция ни вернула, то и будет результатом вызова:

>>> vals = {(1, 2): 1, (2, 3): 2}
>>> def side_effect(*args):
...     return vals[args]
...
>>> mock = MagicMock(side_effect=side_effect)
>>> mock(1, 2)
1
>>> mock(2, 3)
2

Мокирование асинхронных итераторовMocking asynchronous iterators

Начиная с Python 3.8, AsyncMock и MagicMock поддерживают мокирование асинхронных итераторов через __aiter__. Атрибут return_value у __aiter__ можно использовать для задания возвращаемых значений, которые будут использоваться при итерации.

>>> mock = MagicMock()  # AsyncMock также работает здесь
>>> mock.__aiter__.return_value = [1, 2, 3]
>>> async def main():
...     return [i async for i in mock]
...
>>> asyncio.run(main())
[1, 2, 3]

Мокирование асинхронного контекстного менеджераMocking asynchronous context manager

Начиная с Python 3.8, AsyncMock и MagicMock поддерживают мокирование асинхронных контекстных менеджеров через __aenter__ и __aexit__. По умолчанию __aenter__ и __aexit__ являются экземплярами AsyncMock, которые возвращают асинхронную функцию.

>>> class AsyncContextManager:
...     async def __aenter__(self):
...         return self
...     async def __aexit__(self, exc_type, exc, tb):
...         pass
...
>>> mock_instance = MagicMock(AsyncContextManager())  # AsyncMock также работает здесь
>>> async def main():
...     async with mock_instance as result:
...         pass
...
>>> asyncio.run(main())
>>> mock_instance.__aenter__.assert_awaited_once()
>>> mock_instance.__aexit__.assert_awaited_once()

Создание мока на основе существующего объектаCreating a mock from an existing object

Одна из проблем чрезмерного использования мокирования – это связывание тестов с реализацией моков, а не с реальным кодом. Предположим, есть класс, реализующий some_method. В тесте для другого класса вы предоставляете мок этого объекта, который также предоставляет some_method. Если позже вы реорганизуете первый класс так, что он больше не содержит some_method, то тесты продолжат проходить, хотя код уже сломан!

Mock позволяет передать объект в качестве спецификации для мока, используя именованный аргумент spec. Обращение к методам/атрибутам мока, отсутствующим в объекте спецификации, немедленно вызовет ошибку атрибута. Если изменить реализацию спецификации, то тесты, использующие этот класс, сразу начнут падать без необходимости создавать экземпляр класса в этих тестах.

>>> mock = Mock(spec=SomeClass)
>>> mock.old_method()
Traceback (most recent call last):
   ...
AttributeError: Mock object has no attribute 'old_method'. Did you mean: 'class_method'?

Использование спецификации также позволяет более интеллектуально сопоставлять вызовы, сделанные к моку, независимо от того, переданы ли некоторые параметры как позиционные или именованные аргументы:

>>> def f(a, b, c): pass
...
>>> mock = Mock(spec=f)
>>> mock(1, 2, 3)
<Mock name='mock()' id='140161580456576'>
>>> mock.assert_called_with(a=1, b=2, c=3)

Если нужно, чтобы такое интеллектуальное сопоставление работало и для вызовов методов мока, можно воспользоваться авто-спецификацией (auto-speccing).

Если нужна более строгая форма спецификации, запрещающая как установку произвольных атрибутов, так и их получение, можно использовать spec_set вместо spec.

Использование side_effect для возврата содержимого для каждого файлаUsing side_effect to return per file content

mock_open() используется для подмены метода open(). side_effect можно использовать, чтобы возвращать новый объект Mock при каждом вызове. Это позволяет возвращать разное содержимое для каждого файла, хранящегося в словаре:

DEFAULT = "default"
data_dict = {"file1": "data1",
             "file2": "data2"}

def open_side_effect(name):
    return mock_open(read_data=data_dict.get(name, DEFAULT))()

with patch("builtins.open", side_effect=open_side_effect):
    with open("file1") as file1:
        assert file1.read() == "data1"

    with open("file2") as file2:
        assert file2.read() == "data2"

    with open("file3") as file2:
        assert file2.read() == "default"

Декораторы подмены (patch)Patch decorators

Примечание

При использовании patch() важно подменять объекты в том пространстве имён, где они выполняются. Обычно это очевидно, но для краткого руководства прочитайте where to patch.

Частая потребность в тестах – подменить атрибут класса или модуля, например, встроенную функцию или класс в модуле, чтобы проверить, что он инстанциируется. Модули и классы по сути глобальны, поэтому подмену на них нужно отменять после теста, иначе она останется и в других тестах, вызывая труднодиагностируемые проблемы.

mock предоставляет три удобных декоратора для этого: patch(), patch.object() и patch.dict(). patch принимает одну строку вида package.module.Class.attribute для указания подменяемого атрибута. Также опционально принимает значение, на которое нужно заменить атрибут (или класс, или что-то ещё). ‘patch.object’ принимает объект и имя атрибута для подмены, плюс опционально значение для подмены.

patch.object:

>>> original = SomeClass.attribute
>>> @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test():
...     assert SomeClass.attribute == sentinel.attribute
...
>>> test()
>>> assert SomeClass.attribute == original

>>> @patch('package.module.attribute', sentinel.attribute)
... def test():
...     from package.module import attribute
...     assert attribute is sentinel.attribute
...
>>> test()

Если вы подменяете модуль (включая builtins), то используйте patch() вместо patch.object():

>>> mock = MagicMock(return_value=sentinel.file_handle)
>>> with patch('builtins.open', mock):
...     handle = open('filename', 'r')
...
>>> mock.assert_called_with('filename', 'r')
>>> assert handle == sentinel.file_handle, "incorrect file handle returned"

Имя модуля может быть точечным, в форме package.module, если необходимо:

>>> @patch('package.module.ClassName.attribute', sentinel.attribute)
... def test():
...     from package.module import ClassName
...     assert ClassName.attribute == sentinel.attribute
...
>>> test()

Удобный подход – декорировать непосредственно сами тестовые методы:

>>> class MyTest(unittest.TestCase):
...     @patch.object(SomeClass, 'attribute', sentinel.attribute)
...     def test_something(self):
...         self.assertEqual(SomeClass.attribute, sentinel.attribute)
...
>>> original = SomeClass.attribute
>>> MyTest('test_something').test_something()
>>> assert SomeClass.attribute == original

Если нужно подменить с помощью Mock, можно использовать patch() с одним аргументом (или patch.object() с двумя аргументами). Мок будет создан автоматически и передан в тестовую функцию/метод:

>>> class MyTest(unittest.TestCase):
...     @patch.object(SomeClass, 'static_method')
...     def test_something(self, mock_method):
...         SomeClass.static_method()
...         mock_method.assert_called_with()
...
>>> MyTest('test_something').test_something()

Можно накладывать несколько декораторов патча, используя такой шаблон:

>>> class MyTest(unittest.TestCase):
...     @patch('package.module.ClassName1')
...     @patch('package.module.ClassName2')
...     def test_something(self, MockClass2, MockClass1):
...         self.assertIs(package.module.ClassName1, MockClass1)
...         self.assertIs(package.module.ClassName2, MockClass2)
...
>>> MyTest('test_something').test_something()

При вложении декораторов patch моки передаются в декорированную функцию в том же порядке, в котором они применяются (обычный порядок применения декораторов в Python). Это означает снизу вверх, поэтому в примере выше mock для test_module.ClassName2 передаётся первым.

Также есть patch.dict() для установки значений в словаре только в пределах области видимости и восстановления словаря в исходное состояние после завершения теста:

>>> foo = {'key': 'value'}
>>> original = foo.copy()
>>> with patch.dict(foo, {'newkey': 'newvalue'}, clear=True):
...     assert foo == {'newkey': 'newvalue'}
...
>>> assert foo == original

patch, patch.object и patch.dict могут использоваться как контекстные менеджеры.

При использовании patch() для создания мока можно получить ссылку на мок с помощью формы with с ключевым словом “as”:

>>> class ProductionClass:
...     def method(self):
...         pass
...
>>> with patch.object(ProductionClass, 'method') as mock_method:
...     mock_method.return_value = None
...     real = ProductionClass()
...     real.method(1, 2, 3)
...
>>> mock_method.assert_called_with(1, 2, 3)

В качестве альтернативы patch, patch.object и patch.dict можно использовать как декораторы классов. При таком использовании это равносильно применению декоратора к каждому методу, имя которого начинается с “test”.

Дополнительные примерыFurther examples

Вот ещё несколько примеров для немного более сложных сценариев.

Мокирование цепочечных вызововMocking chained calls

Мокирование цепочечных вызовов на самом деле не представляет сложности с mock, если разобраться с атрибутом return_value. Когда мок вызывается впервые (или вы обращаетесь к его return_value до вызова), создаётся новый Mock.

Это означает, что вы можете увидеть, как использовался объект, возвращённый из вызова мокированного объекта, запросив мок return_value:

>>> mock = Mock()
>>> mock().foo(a=2, b=3)
<Mock name='mock().foo()' id='...'>
>>> mock.return_value.foo.assert_called_with(a=2, b=3)

Отсюда один шаг до настройки и проверки утверждений о цепочечных вызовах. Конечно, другая альтернатива – изначально писать код более тестируемым способом…

Итак, предположим, у нас есть код, который выглядит примерно так:

>>> class Something:
...     def __init__(self):
...         self.backend = BackendProvider()
...     def method(self):
...         response = self.backend.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
...         # ещё код

Предполагая, что BackendProvider уже хорошо протестирован, как нам тестировать method()? В частности, мы хотим проверить, что участок кода # more code использует объект ответа правильным образом.

Поскольку эта цепочка вызовов строится из атрибута экземпляра, мы можем применить monkey-patch к атрибуту backend на экземпляре Something. В данном конкретном случае нас интересует только возвращаемое значение из последнего вызова start_call, поэтому нам не нужно много настраивать. Допустим, возвращаемый объект является «файлоподобным», поэтому мы обеспечим, чтобы наш объект ответа использовал встроенный open() в качестве его spec.

Для этого мы создаём экземпляр мока в качестве нашего мок-бэкенда и создаём для него мок-объект ответа. Чтобы установить ответ как возвращаемое значение для этого последнего start_call, мы можем сделать так:

mock_backend.get_endpoint.return_value.create_call.return_value.start_call.return_value = mock_response

Мы можем сделать это чуть более элегантно, используя метод configure_mock() для прямой установки возвращаемого значения:

>>> something = Something()
>>> mock_response = Mock(spec=open)
>>> mock_backend = Mock()
>>> config = {'get_endpoint.return_value.create_call.return_value.start_call.return_value': mock_response}
>>> mock_backend.configure_mock(**config)

С их помощью мы применяем monkey-patch к «mock backend» и можем выполнить реальный вызов:

>>> something.backend = mock_backend
>>> something.method()

Используя mock_calls, мы можем проверить цепочечный вызов одним assert. Цепочечный вызов – это несколько вызовов в одной строке кода, поэтому в mock_calls будет несколько записей. Мы можем использовать call.call_list() для создания этого списка вызовов:

>>> chained = call.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
>>> call_list = chained.call_list()
>>> assert mock_backend.mock_calls == call_list

Частичное мокированиеPartial mocking

В некоторых тестах может потребоваться замокировать вызов datetime.date.today(), чтобы он возвращал известную дату, но при этом не препятствовать созданию новых объектов даты тестируемым кодом. К сожалению, datetime.date написан на C, поэтому нельзя просто применить monkey-patch к статическому методу datetime.date.today().

Вместо этого можно эффективно обернуть класс даты в мок, пропуская вызовы конструктора к реальному классу (и возвращая реальные экземпляры).

Здесь patch decorator используется для мокирования класса date в тестируемом модуле. Затем атрибут side_effect на мок-классе даты устанавливается в лямбда-функцию, возвращающую реальную дату. При вызове мок-класса даты будет создана и возвращена реальная дата с помощью side_effect.

>>> import datetime as dt
>>> with patch('mymodule.date') as mock_date:
...     mock_date.today.return_value = dt.date(2010, 10, 8)
...     mock_date.side_effect = lambda *args, **kw: dt.date(*args, **kw)
...
...     assert mymodule.date.today() == dt.date(2010, 10, 8)
...     assert mymodule.date(2009, 6, 8) == dt.date(2009, 6, 8)

Обратите внимание: мы не патчим datetime.date глобально, мы патчим date в модуле, который его использует. См. где патчить.

При вызове date.today() возвращается известная дата, но вызовы конструктора date(...) по-прежнему возвращают обычные даты. Без этого вам пришлось бы вычислять ожидаемый результат, используя тот же алгоритм, что и тестируемый код, что является классическим антипаттерном тестирования.

Вызовы конструктора даты записываются в атрибуты mock_date (call_count и другие), что также может быть полезно для ваших тестов.

Альтернативный способ мокирования дат или других встроенных классов описан в этой записи блога.

Мокирование метода-генератораMocking a generator method

Генератор Python – это функция или метод, использующие оператор yield для возврата серии значений при итерации по [1].

Метод/функция-генератор вызывается для возврата объекта-генератора. Затем итерируются именно по объекту-генератору. Протокольный метод для итерации – __iter__(), поэтому мы можем замокировать его с помощью MagicMock.

Вот пример класса с методом “iter”, реализованным как генератор:

>>> class Foo:
...     def iter(self):
...         for i in [1, 2, 3]:
...             yield i
...
>>> foo = Foo()
>>> list(foo.iter())
[1, 2, 3]

Как бы мы мокировали этот класс и, в частности, его метод “iter”?

Чтобы настроить значения, возвращаемые из итерации (неявно в вызове list), нам нужно настроить объект, возвращаемый вызовом foo.iter().

>>> mock_foo = MagicMock()
>>> mock_foo.iter.return_value = iter([1, 2, 3])
>>> list(mock_foo.iter())
[1, 2, 3]

Применение одного и того же патча к каждому тестовому методуApplying the same patch to every test method

Если вам нужно установить несколько патчей для нескольких тестовых методов, очевидный способ – применить декораторы patch к каждому методу. Это может показаться излишним повторением. Вместо этого можно использовать patch() (во всех его формах) как декоратор класса. Это применяет патчи ко всем тестовым методам класса. Тестовый метод определяется как метод, имя которого начинается с test:

>>> @patch('mymodule.SomeClass')
... class MyTest(unittest.TestCase):
...
...     def test_one(self, MockSomeClass):
...         self.assertIs(mymodule.SomeClass, MockSomeClass)
...
...     def test_two(self, MockSomeClass):
...         self.assertIs(mymodule.SomeClass, MockSomeClass)
...
...     def not_a_test(self):
...         return 'something'
...
>>> MyTest('test_one').test_one()
>>> MyTest('test_two').test_two()
>>> MyTest('test_two').not_a_test()
'something'

Альтернативный способ управления подстановками – использовать методы patch: start и stop. Они позволяют перенести подстановку в ваши методы setUp и tearDown.

>>> class MyTest(unittest.TestCase):
...     def setUp(self):
...         self.patcher = patch('mymodule.foo')
...         self.mock_foo = self.patcher.start()
...
...     def test_foo(self):
...         self.assertIs(mymodule.foo, self.mock_foo)
...
...     def tearDown(self):
...         self.patcher.stop()
...
>>> MyTest('test_foo').run()

Если используется этот приём, необходимо убедиться, что подстановка «отменена» вызовом stop. Это может быть сложнее, чем кажется, потому что если в setUp возникнет исключение, to tearDown не вызывается. unittest.TestCase.addCleanup() упрощает задачу:

>>> class MyTest(unittest.TestCase):
...     def setUp(self):
...         patcher = patch('mymodule.foo')
...         self.addCleanup(patcher.stop)
...         self.mock_foo = patcher.start()
...
...     def test_foo(self):
...         self.assertIs(mymodule.foo, self.mock_foo)
...
>>> MyTest('test_foo').run()

Мокирование несвязанных методовMocking unbound methods

Иногда тесту требуется подменить несвязанный метод, то есть подменить метод на уровне класса, а не экземпляра. Чтобы проверять, какие объекты вызывали этот конкретный метод, необходимо передавать self в качестве первого аргумента. Проблема в том, что для этого нельзя использовать mock, потому что если заменить несвязанный метод на mock, он не становится связанным методом при получении из экземпляра и поэтому не получает self в качестве аргумента. Обходной путь – подменить несвязанный метод обычной функцией. Декоратор patch() настолько упрощает подмену методов на mock, что необходимость создавать реальную функцию становится обузой.

Если передать autospec=True в patch, то подстановка выполняется с помощью настоящего объекта-функции. Этот объект-функция имеет ту же сигнатуру, что и заменяемая, но внутри делегирует вызовы mock. При этом mock по-прежнему создаётся автоматически, как и раньше. Однако это означает, что если использовать его для подмены несвязанного метода в классе, то подменённая функция при получении из экземпляра превратится в связанный метод. Она получит self в качестве первого аргумента – именно то, что нужно:

>>> class Foo:
...   def foo(self):
...     pass
...
>>> with patch.object(Foo, 'foo', autospec=True) as mock_foo:
...   mock_foo.return_value = 'foo'
...   foo = Foo()
...   foo.foo()
...
'foo'
>>> mock_foo.assert_called_once_with(foo)

Если не использовать autospec=True, то несвязанный метод подменяется экземпляром Mock и не вызывается с self.

Проверка множественных вызовов с помощью mockChecking multiple calls with mock

У mock есть удобный API для проверки того, как используются объекты mock.

>>> mock = Mock()
>>> mock.foo_bar.return_value = None
>>> mock.foo_bar('baz', spam='eggs')
>>> mock.foo_bar.assert_called_with('baz', spam='eggs')

Если mock вызывается только один раз, можно использовать метод assert_called_once_with(), который дополнительно проверяет, что call_count равен единице.

>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
>>> mock.foo_bar()
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
Traceback (most recent call last):
    ...
AssertionError: Expected 'foo_bar' to be called once. Called 2 times.
Calls: [call('baz', spam='eggs'), call()].

И assert_called_with, и assert_called_once_with проверяют последний вызов. Если mock будет вызываться несколько раз и нужно проверить все эти вызовы, можно использовать call_args_list:

>>> mock = Mock(return_value=None)
>>> mock(1, 2, 3)
>>> mock(4, 5, 6)
>>> mock()
>>> mock.call_args_list
[call(1, 2, 3), call(4, 5, 6), call()]

Вспомогательная функция call упрощает проверку этих вызовов. Можно сформировать список ожидаемых вызовов и сравнить его с call_args_list. Это выглядит очень похоже на строковое представление call_args_list:

>>> expected = [call(1, 2, 3), call(4, 5, 6), call()]
>>> mock.call_args_list == expected
True

Работа с изменяемыми аргументамиCoping with mutable arguments

Ещё одна, хоть и редкая, ситуация, которая может доставить неприятности, – когда mock вызывается с изменяемыми аргументами. call_args и call_args_list хранят ссылки на аргументы. Если аргументы изменяются тестируемым кодом, то становится невозможно проверить, какими были значения на момент вызова mock.

Вот пример кода, демонстрирующий проблему. Предположим, в 'mymodule' определены следующие функции:

def frob(val):
    pass

def grob(val):
    "First frob and then clear val"
    frob(val)
    val.clear()

При попытке проверить, что grob вызывает frob с правильным аргументом, происходит следующее:

>>> with patch('mymodule.frob') as mock_frob:
...     val = {6}
...     mymodule.grob(val)
...
>>> val
set()
>>> mock_frob.assert_called_with({6})
Traceback (most recent call last):
    ...
AssertionError: Expected: (({6},), {})
Called with: ((set(),), {})

Одним из возможных решений было бы копирование аргументов, передаваемых в mock. Однако это может вызвать проблемы, если проверки полагаются на тождественность объектов для сравнения.

Вот одно из решений, использующее функциональность side_effect. Если предоставить функцию side_effect для mock, то side_effect будет вызываться с теми же аргументами, что и mock. Это даёт возможность скопировать аргументы и сохранить их для последующих проверок. В этом примере используется другой mock для хранения аргументов, чтобы можно было задействовать методы mock для проверки. Опять же, вспомогательная функция настраивает это:

>>> from copy import deepcopy
>>> from unittest.mock import Mock, patch, DEFAULT
>>> def copy_call_args(mock):
...     new_mock = Mock()
...     def side_effect(*args, **kwargs):
...         args = deepcopy(args)
...         kwargs = deepcopy(kwargs)
...         new_mock(*args, **kwargs)
...         return DEFAULT
...     mock.side_effect = side_effect
...     return new_mock
...
>>> with patch('mymodule.frob') as mock_frob:
...     new_mock = copy_call_args(mock_frob)
...     val = {6}
...     mymodule.grob(val)
...
>>> new_mock.assert_called_with({6})
>>> new_mock.call_args
call({6})

copy_call_args вызывается с mock'ом, который будет вызван. Она возвращает новый mock, на котором выполняется проверка. Функция side_effect создаёт копию аргументов и вызывает наш new_mock с этой копией.

Примечание

Если mock используется только один раз, есть более простой способ проверки аргументов в момент вызова. Можно просто выполнить проверку внутри функции side_effect.

>>> def side_effect(arg):
...     assert arg == {6}
...
>>> mock = Mock(side_effect=side_effect)
>>> mock({6})
>>> mock(set())
Traceback (most recent call last):
    ...
AssertionError

Альтернативный подход – создать подкласс Mock или MagicMock, который копирует аргументы (с помощью copy.deepcopy()). Вот пример реализации:

>>> from copy import deepcopy
>>> class CopyingMock(MagicMock):
...     def __call__(self, /, *args, **kwargs):
...         args = deepcopy(args)
...         kwargs = deepcopy(kwargs)
...         return super().__call__(*args, **kwargs)
...
>>> c = CopyingMock(return_value=None)
>>> arg = set()
>>> c(arg)
>>> arg.add(1)
>>> c.assert_called_with(set())
>>> c.assert_called_with(arg)
Traceback (most recent call last):
    ...
AssertionError: expected call not found.
Expected: mock({1})
Actual: mock(set())
>>> c.foo
<CopyingMock name='mock.foo' id='...'>

При создании подкласса Mock или MagicMock все динамически создаваемые атрибуты и return_value будут автоматически использовать ваш подкласс. Это означает, что все дочерние элементы CopyingMock также будут иметь тип CopyingMock.

Вложенные подстановкиNesting patches

Использовать patch как менеджер контекста удобно, но при множественных подстановках можно получить вложенные операторы with, уходящие всё дальше вправо с отступами:

>>> class MyTest(unittest.TestCase):
...
...     def test_foo(self):
...         with patch('mymodule.Foo') as mock_foo:
...             with patch('mymodule.Bar') as mock_bar:
...                 with patch('mymodule.Spam') as mock_spam:
...                     assert mymodule.Foo is mock_foo
...                     assert mymodule.Bar is mock_bar
...                     assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').test_foo()
>>> assert mymodule.Foo is original

С помощью функций unittest cleanup и методов patch: start и stop можно добиться того же эффекта без вложенных отступов. Простой вспомогательный метод create_patch устанавливает подстановку и возвращает созданный mock:

>>> class MyTest(unittest.TestCase):
...
...     def create_patch(self, name):
...         patcher = patch(name)
...         thing = patcher.start()
...         self.addCleanup(patcher.stop)
...         return thing
...
...     def test_foo(self):
...         mock_foo = self.create_patch('mymodule.Foo')
...         mock_bar = self.create_patch('mymodule.Bar')
...         mock_spam = self.create_patch('mymodule.Spam')
...
...         assert mymodule.Foo is mock_foo
...         assert mymodule.Bar is mock_bar
...         assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').run()
>>> assert mymodule.Foo is original

Мокирование словаря с помощью MagicMockMocking a dictionary with MagicMock

Может потребоваться заменить словарь (или другой контейнер) mock'ом, записывающим все обращения к нему, но при этом сохраняющим поведение словаря.

Это можно сделать с помощью MagicMock, который будет вести себя как словарь, и side_effect, делегирующего доступ к словарю реальному нижележащему словарю, находящемуся под нашим контролем.

При вызове методов __getitem__() и __setitem__() нашего MagicMock (обычный доступ к словарю) вызывается side_effect с ключом (а в случае __setitem__ также и со значением). Также можно управлять возвращаемым значением.

После использования MagicMock можно применять атрибуты, такие как call_args_list, чтобы проверить, как использовался словарь:

>>> my_dict = {'a': 1, 'b': 2, 'c': 3}
>>> def getitem(name):
...      return my_dict[name]
...
>>> def setitem(name, val):
...     my_dict[name] = val
...
>>> mock = MagicMock()
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem

Примечание

Альтернатива использованию MagicMock – применить Mock и предоставить только те магические методы, которые нужны:

>>> mock = Mock()
>>> mock.__getitem__ = Mock(side_effect=getitem)
>>> mock.__setitem__ = Mock(side_effect=setitem)

Третий вариант – использовать MagicMock, передавая dict в качестве аргумента spec (или spec_set), чтобы созданный MagicMock имел только магические методы словаря:

>>> mock = MagicMock(spec_set=dict)
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem

С такими функциями бокового эффекта mock будет вести себя как обычный словарь, но записывать обращения. Он даже вызовет исключение KeyError, если попытаться обратиться к несуществующему ключу.

>>> mock['a']
1
>>> mock['c']
3
>>> mock['d']
Traceback (most recent call last):
    ...
KeyError: 'd'
>>> mock['b'] = 'fish'
>>> mock['d'] = 'eggs'
>>> mock['b']
'fish'
>>> mock['d']
'eggs'

После использования можно делать утверждения о доступе, используя обычные методы и атрибуты mock:

>>> mock.__getitem__.call_args_list
[call('a'), call('c'), call('d'), call('b'), call('d')]
>>> mock.__setitem__.call_args_list
[call('b', 'fish'), call('d', 'eggs')]
>>> my_dict
{'a': 1, 'b': 'fish', 'c': 3, 'd': 'eggs'}

Подклассы Mock и их атрибутыMock subclasses and their attributes

Есть разные причины, по которым может потребоваться создать подкласс Mock. Одна из причин – добавление вспомогательных методов. Вот тривиальный пример:

>>> class MyMock(MagicMock):
...     def has_been_called(self):
...         return self.called
...
>>> mymock = MyMock(return_value=None)
>>> mymock
<MyMock id='...'>
>>> mymock.has_been_called()
False
>>> mymock()
>>> mymock.has_been_called()
True

Стандартное поведение для экземпляров Mock заключается в том, что атрибуты и возвращаемые значения mock имеют тот же тип, что и mock, к которому они обращаются. Это гарантирует, что атрибуты Mock будут Mocks, а атрибуты MagicMockMagicMocks [2]. Поэтому если вы создаёте подкласс для добавления вспомогательных методов, они также будут доступны в атрибутах и возвращаемых значениях экземпляров вашего подкласса.

>>> mymock.foo
<MyMock name='mock.foo' id='...'>
>>> mymock.foo.has_been_called()
False
>>> mymock.foo()
<MyMock name='mock.foo()' id='...'>
>>> mymock.foo.has_been_called()
True

Иногда это неудобно. Например, один пользователь создаёт подкласс mock для адаптера Twisted. Применение этого же поведения к атрибутам приводит к ошибкам.

Mock (во всех его вариантах) использует метод _get_child_mock для создания «под-mock» для атрибутов и возвращаемых значений. Вы можете предотвратить использование вашего подкласса для атрибутов, переопределив этот метод. Сигнатура такова: он принимает произвольные именованные аргументы (**kwargs), которые затем передаются конструктору mock.

>>> class Subclass(MagicMock):
...     def _get_child_mock(self, /, **kwargs):
...         return MagicMock(**kwargs)
...
>>> mymock = Subclass()
>>> mymock.foo
<MagicMock name='mock.foo' id='...'>
>>> assert isinstance(mymock, Subclass)
>>> assert not isinstance(mymock.foo, Subclass)
>>> assert not isinstance(mymock(), Subclass)

Имитация импорта с помощью patch.dictMocking imports with patch.dict

Одна из ситуаций, где имитация сложна – локальный импорт внутри функции. Такие импорты труднее подменить, потому что они не используют объект из пространства имён модуля, который можно замокать.

В целом локальных импортов следует избегать. Иногда их используют для предотвращения циклических зависимостей, для чего обычно есть гораздо лучшее решение (рефакторинг кода), или для снижения «начальных затрат» путём отложенного импорта. Эту проблему также можно решить лучше, чем безусловный локальный импорт (сохранить модуль как атрибут класса или модуля и выполнять импорт только при первом использовании).

Тем не менее, есть способ использовать mock для влияния на результаты импорта. Импорт извлекает объект из словаря sys.modules. Обратите внимание, что извлекается объект, который не обязательно является модулем. При первом импорте модуля в sys.modules помещается объект модуля, поэтому обычно при импорте вы получаете модуль. Однако это не обязательно так.

Это означает, что можно использовать patch.dict(), чтобы временно поместить mock в sys.modules. Любые импорты, выполняемые, пока активен этот patch, будут извлекать mock. Когда patch завершается (декорированная функция завершается, тело with-инструкции выполнено или вызывается patcher.stop()), то, что было там ранее, будет безопасно восстановлено.

Вот пример, который имитирует модуль 'fooble'.

>>> import sys
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
...    import fooble
...    fooble.blob()
...
<Mock name='mock.blob()' id='...'>
>>> assert 'fooble' not in sys.modules
>>> mock.blob.assert_called_once_with()

Как видите, import fooble успешно выполняется, но при выходе в sys.modules не остаётся 'fooble'.

Это также работает для формы from module import name:

>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
...    from fooble import blob
...    blob.blip()
...
<Mock name='mock.blob.blip()' id='...'>
>>> mock.blob.blip.assert_called_once_with()

Немного больше усилий, и можно также имитировать импорт пакетов:

>>> mock = Mock()
>>> modules = {'package': mock, 'package.module': mock.module}
>>> with patch.dict('sys.modules', modules):
...    from package.module import fooble
...    fooble()
...
<Mock name='mock.module.fooble()' id='...'>
>>> mock.module.fooble.assert_called_once_with()

Отслеживание порядка вызовов и менее многословные утверждения о вызовахTracking order of calls and less verbose call assertions

Класс Mock позволяет отслеживать порядок вызовов методов на объектах mock через атрибут method_calls. Это не позволяет отслеживать порядок вызовов между разными объектами mock, но мы можем использовать mock_calls для достижения того же эффекта.

Поскольку mock отслеживают вызовы дочерних mock в mock_calls, а обращение к произвольному атрибуту mock создаёт дочерний mock, мы можем создать отдельные mock из родительского. Вызовы этих дочерних mock будут записаны по порядку в mock_calls родителя:

>>> manager = Mock()
>>> mock_foo = manager.foo
>>> mock_bar = manager.bar
>>> mock_foo.something()
<Mock name='mock.foo.something()' id='...'>
>>> mock_bar.other.thing()
<Mock name='mock.bar.other.thing()' id='...'>
>>> manager.mock_calls
[call.foo.something(), call.bar.other.thing()]

Затем можно делать утверждения о вызовах, включая порядок, сравнивая с атрибутом mock_calls управляющего mock:

>>> expected_calls = [call.foo.something(), call.bar.other.thing()]
>>> manager.mock_calls == expected_calls
True

Если patch создаёт и размещает ваши mock, вы можете прикрепить их к управляющему mock с помощью метода attach_mock(). После прикрепления вызовы будут записаны в mock_calls управляющего.

>>> manager = MagicMock()
>>> with patch('mymodule.Class1') as MockClass1:
...     with patch('mymodule.Class2') as MockClass2:
...         manager.attach_mock(MockClass1, 'MockClass1')
...         manager.attach_mock(MockClass2, 'MockClass2')
...         MockClass1().foo()
...         MockClass2().bar()
<MagicMock name='mock.MockClass1().foo()' id='...'>
<MagicMock name='mock.MockClass2().bar()' id='...'>
>>> manager.mock_calls
[call.MockClass1(),
call.MockClass1().foo(),
call.MockClass2(),
call.MockClass2().bar()]

Если было много вызовов, но вас интересует только определённая последовательность, можно использовать метод assert_has_calls(). Он принимает список вызовов (созданный с помощью объекта call). Если эта последовательность вызовов есть в mock_calls, утверждение проходит.

>>> m = MagicMock()
>>> m().foo().bar().baz()
<MagicMock name='mock().foo().bar().baz()' id='...'>
>>> m.one().two().three()
<MagicMock name='mock.one().two().three()' id='...'>
>>> calls = call.one().two().three().call_list()
>>> m.assert_has_calls(calls)

Даже если цепочка вызовов m.one().two().three() – не единственные вызовы, сделанные к mock, утверждение всё равно проходит.

Иногда mock получает несколько вызовов, а вас интересуют утверждения только о некоторых из них. Возможно, вас не волнует порядок. В этом случае можно передать any_order=True в assert_has_calls:

>>> m = MagicMock()
>>> m(1), m.two(2, 3), m.seven(7), m.fifty('50')
(...)
>>> calls = [call.fifty('50'), call(1), call.seven(7)]
>>> m.assert_has_calls(calls, any_order=True)

Более сложное сопоставление аргументовMore complex argument matching

Используя ту же базовую концепцию, что и ANY, можно реализовать сопоставители для более сложных утверждений об объектах, передаваемых имитациям в качестве аргументов.

Предположим, мы ожидаем, что mock будет передан некоторый объект, который по умолчанию сравнивается по идентичности (это поведение Python по умолчанию для пользовательских классов). Чтобы использовать assert_called_with(), нужно передать точно такой же объект. Если нас интересуют только некоторые атрибуты этого объекта, можно создать сопоставитель, который будет проверять эти атрибуты.

В этом примере видно, что «стандартного» вызова assert_called_with недостаточно:

>>> class Foo:
...     def __init__(self, a, b):
...         self.a, self.b = a, b
...
>>> mock = Mock(return_value=None)
>>> mock(Foo(1, 2))
>>> mock.assert_called_with(Foo(1, 2))
Traceback (most recent call last):
    ...
AssertionError: expected call not found.
Expected: mock(<__main__.Foo object at 0x...>)
Actual: mock(<__main__.Foo object at 0x...>)

Функция сравнения для класса Foo может выглядеть примерно так:

>>> def compare(self, other):
...     if not type(self) == type(other):
...         return False
...     if self.a != other.a:
...         return False
...     if self.b != other.b:
...         return False
...     return True
...

А объект-сопоставитель, который может использовать такие функции сравнения для операции равенства, будет выглядеть примерно так:

>>> class Matcher:
...     def __init__(self, compare, some_obj):
...         self.compare = compare
...         self.some_obj = some_obj
...     def __eq__(self, other):
...         return self.compare(self.some_obj, other)
...

Собираем всё вместе:

>>> match_foo = Matcher(compare, Foo(1, 2))
>>> mock.assert_called_with(match_foo)

Matcher создаётся с нашей функцией сравнения и объектом Foo, с которым мы хотим сравнивать. В assert_called_with будет вызван метод равенства Matcher, который сравнивает объект, с которым был вызван mock, с тем, с которым мы создали наш сопоставитель. Если они совпадают, assert_called_with проходит, иначе возбуждается AssertionError:

>>> match_wrong = Matcher(compare, Foo(3, 4))
>>> mock.assert_called_with(match_wrong)
Traceback (most recent call last):
    ...
AssertionError: Expected: ((<Matcher object at 0x...>,), {})
Called with: ((<Foo object at 0x...>,), {})

При небольшой доработке можно заставить функцию сравнения напрямую возбуждать AssertionError и выдавать более полезное сообщение об ошибке.

Начиная с версии 1.5, библиотека тестирования Python PyHamcrest предоставляет аналогичную функциональность, которая может быть полезна здесь, в виде средства сравнения на равенство (hamcrest.library.integration.match_equality).