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

Added in version 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)

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

Возбуждение исключений с помощью mocks

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Декораторы

Примечание

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

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

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

patch.object:

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

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

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

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

При необходимости в имени модуля можно поставить точку в виде package.module:

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

Хорошим паттерном является украшение самих методов тестирования:

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

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

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

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

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

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

Это означает, что вы можете увидеть, как использовался объект, возвращенный в результате вызова 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()
...         # 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, и поэтому я не мог просто подружиться со статическим методом datetime.date.today().

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

Здесь 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]

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

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

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

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

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

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

Примечание

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

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

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

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

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

Матрешки

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

>>> 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, но передать в качестве аргумента spec (или spec_set) dict, чтобы в созданном MagicMock были доступны только магические методы словаря:

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

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

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

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

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

Подклассы Mock и их атрибуты

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

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

Сопоставление импорта с помощью 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 будут записываться по порядку в 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 not found.
Expected: mock(<__main__.Foo object at 0x...>)
Actual: mock(<__main__.Foo object at 0x...>)

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

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

А объект 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).