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

После вызова 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)

Имитация вызовов методов для объекта

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

Приведенный ниже простой метод 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 является результатом вызова 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'

Называя свои насмешки

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

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

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

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

Создание исключений с помощью mocks

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

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

Функции побочных эффектов и повторяющиеся элементы

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

>>> 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 поддерживают имитацию Асинхронные итераторы через __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, 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()

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

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

Использование side_effect для возврата содержимого каждого файла

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

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

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

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

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

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

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

Примечание

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

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

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

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

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

Для этого мы создаем макетный экземпляр в качестве нашего макетного серверного модуля и создаем для него макетный объект response. Чтобы задать response в качестве возвращаемого значения для этого конечного 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, мы можем проверить цепной вызов с помощью одного assert. Цепной вызов - это несколько вызовов в одной строке кода, поэтому в mock_calls будет несколько записей. Мы можем использовать call.call_list(), чтобы создать этот список звонков для нас:

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

Частичное издевательство

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

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

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

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

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

Имитация метода генератора

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

Вызывается метод / функция генератора, возвращающая объект generator. Затем выполняется итерация по объекту generator. Метод протокола для итерации - __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]

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

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

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

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

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

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

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

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

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

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

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

Вот несколько примеров кода, показывающих проблему. Представьте, что в «mymodule» определены следующие функции:

def frob(val):
    pass

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

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

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

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

Вот одно из решений, которое использует функциональность side_effect. Если вы предоставите функцию side_effect для макета, то side_effect будет вызываться с теми же аргументами, что и у макета. Это дает нам возможность скопировать аргументы и сохранить их для последующих утверждений. В этом примере я использую another 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 вызывается с макетом, который будет вызван. Он возвращает новый макет, для которого мы выполняем утверждение. Функция side_effect создает копию аргументов и вызывает наш new_mock с копией.

Примечание

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

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

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

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

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

Гнездящиеся участки

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

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

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

>>> 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 заключается в том, что атрибуты и возвращаемое значение mocks имеют тот же тип, что и mock, по которому к ним осуществляется доступ. Это гарантирует, что атрибуты Mock являются атрибутами Mocks, а атрибуты MagicMock являются атрибутами MagicMocks [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

Иногда это неудобно. Например, 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)

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

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

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

Кроме того, есть способ использовать mock, чтобы повлиять на результаты импорта. При импорте извлекается объект из словаря sys.modules. Обратите внимание, что при этом извлекается объект, который не обязательно должен быть модулем. При первом импорте модуля объект module помещается в 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() - это не единственные вызовы, которые были выполнены для mock, assert все равно выполняется успешно.

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

Предположим, мы ожидаем, что некоторый объект будет передан в 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 создается с помощью нашей функции compare и объекта Foo, с которым мы хотим провести сравнение. В assert_called_with будет вызван метод Matcher, который сравнивает объект, с которым был вызван mock, с тем, с которым мы создали наше средство сопоставления. Если они совпадают, то assert_called_with проходит, а если нет, то возникает AssertionError:

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

Немного подправив, вы могли бы заставить функцию сравнения напрямую выводить значение AssertionError и выдавать более полезное сообщение об ошибке.

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

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