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

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

Использование макета

Моделирование методов исправления

Обычное использование объектов 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“ на нашем макете. Доступ к close создает его. Таким образом, если метод „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 - а также к их дочерним атрибутам.

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

Начиная с 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 also works here
>>> 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. В тесте для другого класса вы предоставляете имитатор этого объекта, который также обеспечивает 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)

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

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

Декораторы пластырей

Примечание

При использовании 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:

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

Если вы хотите установить патч с помощью макета, вы можете использовать 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()

При вложении декораторов патчей имитаторы передаются в декорированную функцию в том же порядке, в котором они применялись (обычный 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».

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

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

Издевательство над прикованными вызовами

Издевательство над цепочкой вызовов на самом деле простое с помощью mock, если вы понимаете атрибут return_value. Когда mock вызывается в первый раз или вы получаете его 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()
...         # more code

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

Поскольку эта цепочка вызовов выполняется из атрибута экземпляра, мы можем по-обезьяньи подправить атрибут backend на экземпляре Something. В данном конкретном случае нас интересует только возвращаемое значение от последнего вызова start_call, поэтому нам не нужно много конфигурировать. Предположим, что возвращаемый объект является «файлоподобным», поэтому мы убедимся, что наш объект ответа использует встроенный open() в качестве 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)

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

>>> 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 в макет, но передавать вызовы конструктора реальному классу (и возвращать реальные экземпляры).

Здесь patch decorator используется для имитации класса 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 в модуле, который использует его. См. where to patch.

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

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

Альтернативный способ работы с mocking dates, или другими встроенными классами, обсуждается в this blog entry.

Издевательство над методом генератора

Генератор 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

Существуют также выражения генераторов и более advanced uses генераторов, но здесь мы их не рассматриваем. Очень хорошее введение в генераторы и их возможности - это: Generator Tricks for Systems Programmers.

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

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

Альтернативным способом управления патчами является использование методы исправления: запуск и остановка. Они позволяют переместить исправления в методы 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()

Издевательство над несвязанными методами

Сегодня во время написания тестов мне понадобилось исправить unbound метод (исправление метода на класс, а не на экземпляр). Мне нужно было, чтобы в качестве первого аргумента передавался self, потому что я хотел сделать утверждения о том, какие объекты вызывают этот конкретный метод. Проблема в том, что для этого нельзя использовать макет, потому что если вы замените несвязанный метод макетом, он не станет связанным методом при извлечении из экземпляра, и поэтому в него не будет передаваться 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.

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

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

Если ваш имитатор вызывается только один раз, вы можете использовать метод 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

Работа с изменчивыми аргументами

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

Вот пример кода, который показывает проблему. Представьте себе следующие функции, определенные в „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 для имитатора, то функция 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 делает копию args и вызывает нашу 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().__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 и методы исправления: запуск и остановка мы можем добиться того же эффекта без вложенных отступов. Простой вспомогательный метод 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

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

Мы можем сделать это с помощью 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, а 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

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

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

Исключением из этого правила являются не вызываемые макеты. Атрибуты используют вариант callable, потому что иначе не вызываемые имитаторы не могли бы иметь вызываемые методы.

Сопряжение импорта с помощью 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 не остается «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()

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

Класс 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
...

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

>>> 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, с тем, с которым мы создали наш 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).

Вернуться на верх