unittest.mock — Начало работы

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

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

Способы Mock патчинга

Общие варианты использования объектов Mock включают:

  • Методы патчинга
  • Запись вызываемых методов объектов

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

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

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

Примечание

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

После вызова мок его атрибут 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 для вызовов методов объекта

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

У простого 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» ещё не был вызван, доступ к нему в тесте создаст его, но assert_called_with() вызовет исключение сбоя.

Мокинг классы

Типичный вариант использования — мок из классов, созданных вашим тестируемым кодом. Когда патчите класс, данный класс заменяется на мок. Экземпляры создаваемым вызовам класса. Это означает, что вы получаете доступ к «мок экземпляру», просматривая возвращаемое значение фиктивного класса.

В приведённом ниже примере у нас есть функция some_function, которая создаёт экземпляр Foo и вызывает для него метод. Вызов patch() заменяет класс Foo на мок. Экземпляр Foo является результатом вызова мок, поэтому он настраивается путём изменения мок 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'

Именование своих мокингов

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

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

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

Часто нужно отслеживать более одного вызова метода. Атрибут mock_calls записывает все вызовы дочерних атрибутов мок, а также их дочерних элементов.

>>> 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

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

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

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

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

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

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

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

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

>>> 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(), который превращает наш объект вызова в список вызовов, представляющих связанные вызовы.

Вызов исключений с мокингами

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

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

Функции и итерации побочных эффектов

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

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

Начиная с 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]

Мокинг асинхронного менеджера контекста

Начиная с 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()

Создание Mock из существующего объекта

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

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

>>> mock = Mock(spec=SomeClass)
>>> mock.old_method()
Traceback (most recent call last):
   ...
AttributeError: object has no attribute 'old_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)

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

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

Декораторы патчей

Примечание

Для 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() с двумя аргументами). Mock будет создан для вас и передан в функцию / метод тестирования:

>>> 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()

При вложении декораторов патчей мокинги передаются в декорированную функцию в том же порядке, в котором они применялись (обычный порядок применения декораторов в Python). Это означает снизу вверх, поэтому в приведённом выше примере сначала передаётся мок для 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() для создания для вас мок, вы можете получить ссылку на мок, используя форму «as» в операторе with:

>>> 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».

Дальнейшие примеры

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

Мокинг цепочечных вызовов

Имитировать цепочечные вызовы с мок на самом деле просто, если вы понимаете атрибут 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 правильно использует объект ответа.

Поскольку цепочка вызовов выполняется из атрибута экземпляра, мы можем монки патчить атрибут 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)

С их помощью мы монки патчим «бэкэнд мок» на месте и можем сделать настоящий вызов:

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

Используя mock_calls, мы можем проверить связанный вызов с помощью одного утверждения. Связанный вызов — это несколько вызовов в одной строке кода, поэтому в 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

Частичный мокинг

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

Я нашёл простой способ сделать это, который включал эффективную обертку класса даты с помощью мок, но передавая вызовы конструктора реальному классу (и возвращая реальные экземпляры).

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

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

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

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

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

Мокинг метода генератора

Генератор 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]
[1]Есть также выражения-генераторы и более сложные варианты использования генераторов, но мы не беспокоимся о них здесь.

Применение одного и того же патча ко всем проверяемым методам

Если вы хотите установить несколько патчей для нескольких методов тестирования, очевидный способ — применить декораторы патчей к каждому методу. Это может показаться ненужным повторением. Для Python 2.6 или более поздних версий вы можете использовать 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: запуск и остановка. Это позволяет патчить методы 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 вызывается исключение, 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()

Имитация несвязанных методов

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

Если вы передадите autospec=True для патчинга, он выполнит исправление с помощью функции настоящего объекта. У данного объекта функции та же сигнатура, что он заменяет, но делегирует мок под капотом. Вы по-прежнему получаете свой мок автоматически созданный точно так же, как и раньше. Однако это означает, что если вы используете его для патчинга несвязанного метода в классе, мок функция будет превращена в связанный метод, если она получена из экземпляра. У неё будет 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.

Проверка нескольких вызовов с помощью мок

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

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

Если ваш мок вызывается только один раз, вы можете использовать метод 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 to be called once. Called 2 times.

И assert_called_with, и assert_called_once_with делают утверждения о вызове самого последнего. Если ваш мок будет вызывать несколько раз, и вы хотите сделать утверждения о всех данных вызовах, вы можете использовать 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. Это очень похоже на repr call_args_list:

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

Как справляться с изменчивыми аргументами

Другая ситуация, которая встречается редко, но может вас укусить, — это когда ваш мок вызывается с изменяемыми аргументами. call_args и call_args_list хранят ссылки в аргументах. Если аргументы изменяются тестируемым кодом, вы больше не можете утверждать, какими были значения при вызове мок.

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

def frob(val):
    pass

def grob(val):
    "Сначала frob, а затем чистое 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(),), {})

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

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

>>> 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 вызывается вместе с мок, который будет вызван. Он возвращает новый мок, для которого мы выполняем утверждение. Функция side_effect делает копию аргументов и вызывает наш new_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(CopyingMock, self).__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: mock({1})
Actual call: mock(set())
>>> c.foo
<CopyingMock name='mock.foo' id='...'>

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

Вложенные патчи

Использование patch в качестве менеджера контекста — это хорошо, но если вы сделаете несколько патчей, вы можете получить вложенные операторы с отступом все дальше и дальше вправо:

>>> 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: запуск и остановка мы можем добиться того же эффекта без вложенных отступов. Простой вспомогательный метод create_patch устанавливает патч и возвращает нам созданный мок:

>>> 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

Мокинг словаря с помощью MagicMock

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

Мы можем сделать это с помощью 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.__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. Одна из причин может заключаться в добавлении вспомогательных методов. Вот глупый пример:

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

>>> 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)
[2]Исключением из этого правила является невызываемый мокинги. Атрибуты используют расширение вызываемый вариант, потому что иначе не вызываемые мокинги не могут содержать вызываемых методов.

Мокинг импорта с помощью patch.dict

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

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

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

Это означает, что можно использовать patch.dict(), чтобы временно поставить мок на место в sys.modules. Любой импорт, пока данный патч активен, приведёт к получению мока. Когда патч завершён (декорированная функция завершается, тело оператора 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 не остаётся никаких ошибок.

Это также работает для формы 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()

Отслеживание порядка вызовов и менее подробные утверждения вызовов

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

Поскольку мокинги отслеживают вызовы дочерних мокингов в mock_calls, а доступ к произвольному атрибуту мок создаёт дочерний мок, мы можем создать отдельные мокинги из родительского. После этого все вызовы данных дочерних моков будут записаны по порядку в 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 в мок менеджере:

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

Если patch создаёт и устанавливает ваш мокинги, вы можете присоединить их к менеджеру мок, используя метод 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() — не единственный вызов, который был сделан в мок, утверждение все же выполняется.

Иногда к моку может быть выполнено несколько вызовов, и вас интересует только утверждение о некоторых из данных вызовов. Вы можете даже не заботиться о порядке. В этом случае вы можете передать 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)

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

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

Предположим, мы ожидаем, что какой-то объект будет передан в мок, который по умолчанию сравнивает одинаковые на основе идентичности объекта (что является значением 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(<__main__.Foo object at 0x...>)
Actual call: call(<__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, с которым был вызван мок, с тем, с которым мы создали наш сопоставитель. Если они совпадают, то 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).