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='...'>

Как только наш 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 для вызовов метода для объекта

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

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

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

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

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

Нам не нужно делать никакой работы, чтобы обеспечить метод «закрытия» на нашей mock. Доступ к закрытию создает его. Таким образом, если «close» еще не был вызван, то доступ к нему в тесте создаст его, но assert_called_with() вызовет исключение сбоя.

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

Обычным примером использования является mock классов, созданных тестируемым кодом. При исправлении класса этот класс заменяется mock. Сущности создаются вызовом класса. Это означает, что доступ к «mock сущность» осуществляется путем просмотра возвращаемого значения mocked класса.

В примере ниже мы имеем функцию 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'

Имя вашего моки

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

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

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

Часто требуется отслеживать несколько вызовов метода. В 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

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

Установить возвращаемое значение для объекта 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 более сложной ситуации, как, например, 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 = Mock(side_effect=Exception('Boom!'))
>>> mock()
Traceback (most recent call last):
  ...
Exception: Boom!

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

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

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

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

>>> 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 оказывают поддержку мока Асинхронные контекстные менеджеры через __aenter__. return_value атрибут __aiter__ может быть используемый, чтобы установить возвращает значения быть используемый для повторения.

>>> mock = MagicMock()  # AsyncMock also works here
>>> 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 оказывают поддержку mock Асинхронные контекстные менеджеры через __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()

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

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

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

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

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

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

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

Если вы хотите более сильную форму спецификации, которая предотвращает установку произвольных атрибуты, а также их получение, то вы можете использовать установить spec вместо 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, в котором декораторы применяются). Это означает, что снизу вверх, так что в приведенном выше примере 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() для создания mock можно получить ссылку на mock с помощью формы «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».

Другие примеры

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

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

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

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

>>> 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()? В частности, мы хотим проверить, что раздел кода # больше кода использует объект ответа правильным образом.

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

Для этого создается mock сущность в качестве mock бэкэнда и для него создается объект ответа mock. Чтобы установить ответ в качестве возвращает значение для этого окончательного 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)

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

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

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

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

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

Здесь используемый декоратор patch для mock класса date в тестируемом модуле. Затем side_effect атрибут в классе дат mock устанавливается в лямбда-функцию, которая возвращает реальную дату. Когда класс даты mock называется, реальная дата будет создана и возвращенный 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__(), поэтому мы можем mock это с помощью MagicMock.

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

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

Как бы мы mock этот класс, и в частности его метод «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: 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. Это может быть более точным, чем вы думаете, потому что если в настройке Up возникает исключение, то 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 в качестве первого аргумента, потому что я хочу сделать утверждения о том, какие объекты вызывались этим методом. Проблема в том, что вы не можете патчить mock для этого, потому что если вы заменяете несвязанный метод на mock, он не становится связанным методом при извлечении из сущность, и поэтому он не получает себя. Обходной путь состоит в исправлении несвязанного метода действительной функцией. Декоратор patch() делает так просто латать методы с mock, что необходимость создания реальной функции становится неприятностью.

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

Проверка нескольких вызовов с помощью 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 to be called once. Called 2 times.

И 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. Это выглядит удивительно похоже на repr call_args_list:

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

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

Другая ситуация редкая, но может укусить вас, это когда ваш 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 функциональные возможности. Если предусмотрена mock функция side_effect, то 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(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.

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

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

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

Мокинг словаря с 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)

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

>>> 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 атрибуты - Mocks, и MagicMock атрибуты MagicMocks [2]. Так что если вы подклассифицируете, чтобы добавить вспомогательные методы, то они также будут доступны на атрибуты и возвращает значение mock сущности вашего подкласса.

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

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

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

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

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

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

Поскольку моки отслеживаем вызовы дочерних моков в mock_calls, а доступ к произвольному атрибут 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 [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 менеджера с помощью метода 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)

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

Используя ту же самую базовую концепцию, что и 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(<__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 равенства, который сравнивает объект, с которым был вызван 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, библиотека PyHamcrest тестирования Python предоставляет аналогичные функциональные возможности, которые могут быть полезны здесь, в виде своего устройства согласования равенства (hamcrest.library.integration.match_equality).