unittest.mock
— приступая к работе¶
Добавлено в версии 3.3.
Использование Mock¶
Mock патчинг методов¶
К обычным видам использования Mock
объектов относятся:
- Методы патчинга
- Вызов метода записи на объектах
Можно заменить метод объекта, чтобы проверить, что он вызван с правильными аргументами другой частью системы:
>>> real = SomeClass()
>>> real.method = MagicMock(name='method')
>>> real.method(3, 4, 5, key='value')
<MagicMock name='method()' id='...'>
Как только наш mock был используем (real.method
в этом примере), он
содержит методы и атрибуты, которые позволяют сделать утверждения о том, как он
был использован.
Примечание
В большинстве этих примеров классы Mock
и MagicMock
являются
взаимозаменяемыми. Поскольку MagicMock
является более способным классом, он
делает разумным для использования по умолчанию.
После вызова mock для его called
атрибута устанавливается значение
True
. Что более важно, мы можем использовать метод assert_called_with()
или
assert_called_once_with()
, чтобы проверить, что он был вызван с правильными аргументами.
В этом примере проверяется, что вызов ProductionClass().method
приводит к вызову метода
something
:
>>> class ProductionClass:
... def method(self):
... self.something(1, 2, 3)
... def something(self, a, b, c):
... pass
...
>>> real = ProductionClass()
>>> real.something = MagicMock()
>>> real.method()
>>> real.something.assert_called_once_with(1, 2, 3)
Mock для вызовов метода для объекта¶
В последнем примере мы пропатчили метод непосредственно на объекте, чтобы проверить, правильно ли он вызван. Другим распространенным примером использования является передача объекта в метод (или некоторую часть тестируемой системы), а затем проверка правильности его используемый.
Простой ProductionClass
ниже имеет closer
метод. Если он вызывается с объектом,
он вызывает close
по нему.
>>> class ProductionClass:
... def closer(self, something):
... something.close()
...
Таким образом, чтобы проверить его, нам нужно пройти в объект с close
методом и проверить, что он был вызван правильно.
>>> real = ProductionClass()
>>> mock = Mock()
>>> real.closer(mock)
>>> mock.close.assert_called_with()
Нам не нужно делать никакой работы, чтобы обеспечить метод «закрытия» на нашей
mock. Доступ к закрытию создает его. Таким образом, если «close» еще не
был вызван, то доступ к нему в тесте создаст его, но assert_called_with()
вызовет исключение сбоя.
Мокинг классов¶
Обычным примером использования является mock классов, созданных тестируемым кодом. При исправлении класса этот класс заменяется mock. Сущности создаются вызовом класса. Это означает, что доступ к «mock сущность» осуществляется путем просмотра возвращаемого значения mocked класса.
В примере ниже мы имеем функцию some_function
, которая создает экземпляр
Foo
и вызывает метод для него. Вызов patch()
заменяет Foo
класса на mock. Foo
сущность является результатом вызова
mock, поэтому он настраивается путем изменения mock return_value
:
>>> def some_function():
... instance = module.Foo()
... return instance.method()
...
>>> with patch('module.Foo') as mock:
... instance = mock.return_value
... instance.method.return_value = 'the result'
... result = some_function()
... assert result == 'the result'
Имя вашего моки¶
Может быть полезно дать своим мокам имя. Имя отображается в repr mock и может быть полезным, когда mock появляется в сообщениях об ошибках теста. Имя также распространяется на атрибуты или методы mock:
>>> mock = MagicMock(name='foo')
>>> mock
<MagicMock name='foo' id='...'>
>>> mock.method
<MagicMock name='foo.method' id='...'>
Отслеживание всех вызовов¶
Часто требуется отслеживать несколько вызовов метода. В mock_calls
атрибут
записываются все вызовы дочерним атрибутом mock - а также их потомкам.
>>> mock = MagicMock()
>>> mock.method()
<MagicMock name='mock.method()' id='...'>
>>> mock.attribute.method(10, x=53)
<MagicMock name='mock.attribute.method()' id='...'>
>>> mock.mock_calls
[call.method(), call.attribute.method(10, x=53)]
Если сделать утверждение о mock_calls
и какие-либо неожиданные методы были
вызваны, то утверждение потерпит неудачу. Это полезно, поскольку, а также
утверждая, что ожидаемые вызовы были сделаны, вы также проверяете, что они были
сделаны в правильном порядке и без дополнительных вызовов:
Объект call
используется для создания списков для сравнения с
mock_calls
:
>>> expected = [call.method(), call.attribute.method(10, x=53)]
>>> mock.mock_calls == expected
True
Однако параметры вызовов, которые возвращает моки, не записываются, что означает невозможность отслеживания вложенных вызовов, где важны параметры, используемый для создания предков:
>>> m = Mock()
>>> m.factory(important=True).deliver()
<Mock name='mock.factory().deliver()' id='...'>
>>> m.mock_calls[-1] == call.factory(important=False).deliver()
True
Установка возвращаемых значений и атрибутов¶
Установить возвращаемое значение для объекта mock тривиально просто:
>>> mock = Mock()
>>> mock.return_value = 3
>>> mock()
3
Конечно, вы можете сделать то же самое для методов на mock:
>>> mock = Mock()
>>> mock.method.return_value = 3
>>> mock.method()
3
возвращает значение также можно задать в конструкторе:
>>> mock = Mock(return_value=3)
>>> mock()
3
Если вам нужна настройка атрибута на вашем mock, просто сделайте это:
>>> mock = Mock()
>>> mock.x = 3
>>> mock.x
3
Иногда хочется mock более сложной ситуации, как, например, mock.connection.cursor().execute("SELECT 1")
.
Если требуется, чтобы этот вызов возвращает список, необходимо настроить
результат вложенного вызова.
Мы можем использовать call
для построения набора вызовов в «цепочечном
вызове», как это для простого утверждения после:
>>> mock = Mock()
>>> cursor = mock.connection.cursor.return_value
>>> cursor.execute.return_value = ['foo']
>>> mock.connection.cursor().execute("SELECT 1")
['foo']
>>> expected = call.connection.cursor().execute("SELECT 1").call_list()
>>> mock.mock_calls
[call.connection.cursor(), call.connection.cursor().execute('SELECT 1')]
>>> mock.mock_calls == expected
True
Именно вызов в .call_list()
превращает наш объект вызова в список вызовов,
представляющих последовательные вызовы.
Возбуждение исключений с моки¶
Полезный атрибут - side_effect
. Если задать для него класс исключения или
сущность, то исключение будет создано при вызове mock.
>>> mock = Mock(side_effect=Exception('Boom!'))
>>> mock()
Traceback (most recent call last):
...
Exception: Boom!
Функции побочных эффектов и итерабельности¶
side_effect
также может быть установлена в функцию или итерабельный объект.
Сценарий использования side_effect
в качестве итератора - это случай, когда
mock будет вызываться несколько раз, и каждый вызов должен возвращает
другое значение. При установке для side_effect
значения итератора каждый вызов
mock возвращает следующий значение из итератора:
>>> mock = MagicMock(side_effect=[4, 5, 6])
>>> mock()
4
>>> mock()
5
>>> mock()
6
Для более продвинутых сценариев использования, например, динамического изменения
возвращает значения в зависимости от того, с чем вызывается mock,
side_effect
может быть функцией. Функция будет вызвана с теми же аргументами,
что и mock. Безотносительно функция возвращает то, что возвращает вызов:
>>> vals = {(1, 2): 1, (2, 3): 2}
>>> def side_effect(*args):
... return vals[args]
...
>>> mock = MagicMock(side_effect=side_effect)
>>> mock(1, 2)
1
>>> mock(2, 3)
2
Мокинг асинхронных итераторов¶
СPython 3.8, AsyncMock
и MagicMock
оказывают поддержку мока Асинхронные контекстные менеджеры
через __aenter__
. return_value
атрибут __aiter__
может быть используемый,
чтобы установить возвращает значения быть используемый для повторения.
>>> mock = MagicMock() # AsyncMock also works here
>>> mock.__aiter__.return_value = [1, 2, 3]
>>> async def main():
... return [i async for i in mock]
...
>>> asyncio.run(main())
[1, 2, 3]
Мокинг асинхронного диспетчера контекст¶
С Python 3.8 AsyncMock
и MagicMock
оказывают поддержку mock Асинхронные контекстные менеджеры
через __aenter__
и __aexit__
. По умолчанию __aenter__
и __aexit__
являются AsyncMock
сущности, которые возвращает асинхронной функцией.
>>> class AsyncContextManager:
... async def __aenter__(self):
... return self
... async def __aexit__(self, exc_type, exc, tb):
... pass
...
>>> mock_instance = MagicMock(AsyncContextManager()) # AsyncMock также работает здесь
>>> async def main():
... async with mock_instance as result:
... pass
...
>>> asyncio.run(main())
>>> mock_instance.__aenter__.assert_awaited_once()
>>> mock_instance.__aexit__.assert_awaited_once()
Создание макета из существующего объекта¶
Одна из проблем с чрезмерным использованием мокинга заключается в том, что
он связывает ваши тесты с реализацией вашими моками, а не с реальным
кодом. Предположим, у вас есть класс, который реализует some_method
. В
тесте для другого класса указывается mock этого объекта, который
также предоставляет some_method
. Если позже вы проведете рефакторинг
первого класса, чтобы он больше не some_method
- то ваши тесты продолжат
проходить даже несмотря на то, что ваш код сейчас сломан!
Mock
позволяет предоставить объект в качестве спецификации для
mock, используя аргумент spec ключевой. Доступ к методам/
атрибутам на mock, которые не существуют в объекте спецификации,
немедленно вызовет ошибку атрибут. Если изменить реализацию спецификации, то
тесты, использующие этот класс, начнут завершаться сбоем немедленно без
необходимости создания экземпляра класса в этих тестах.
>>> mock = Mock(spec=SomeClass)
>>> mock.old_method()
Traceback (most recent call last):
...
AttributeError: object has no attribute 'old_method'
Использование спецификации также позволяет более интеллектуально сопоставлять вызовы, сделанные в mock, независимо от того, передавались ли некоторые параметры в качестве позиционных или именованных аргументов:
>>> def f(a, b, c): pass
...
>>> mock = Mock(spec=f)
>>> mock(1, 2, 3)
<Mock name='mock()' id='140161580456576'>
>>> mock.assert_called_with(a=1, b=2, c=3)
Если требуется, чтобы это более интеллектуальное сопоставление также работало с вызовами методов на mock, можно использовать auto-speccing.
Если вы хотите более сильную форму спецификации, которая предотвращает установку произвольных атрибуты, а также их получение, то вы можете использовать установить spec вместо spec.
Декораторы патчей¶
Примечание
При patch()
важно, чтобы объекты исправлялись в пространстве имен, где они
просматриваются. Обычно это просто, но для быстрого руководства читать
где патчить.
Обычная потребность в тестах состоит в исправлении атрибут класса или атрибут модуля, например, исправлении сборки или исправлении класса в модуле для проверки того, что он создан. Модули и классы фактически глобальны, поэтому патч на них должно быть отменено после теста, или патч сохранится в других тестах и вызовет затруднения в диагностике проблем.
mock предоставляет для этого три удобных декоратора: patch()
,
patch.object()
и patch.dict()
. patch
строка формы используется один
package.module.Class.attribute
, чтобы указать атрибут для исправления. Кроме того, может
потребоваться значение, на которое требуется заменить атрибут (или класс
или что-либо еще). «patch.object» принимает объект и имя атрибут, который
требуется исправить, а также, при необходимости, значение для исправления.
patch.object
:
>>> original = SomeClass.attribute
>>> @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test():
... assert SomeClass.attribute == sentinel.attribute
...
>>> test()
>>> assert SomeClass.attribute == original
>>> @patch('package.module.attribute', sentinel.attribute)
... def test():
... from package.module import attribute
... assert attribute is sentinel.attribute
...
>>> test()
Если выполняется патч модуля (включая builtins
), используйте
patch()
вместо patch.object()
:
>>> mock = MagicMock(return_value=sentinel.file_handle)
>>> with patch('builtins.open', mock):
... handle = open('filename', 'r')
...
>>> mock.assert_called_with('filename', 'r')
>>> assert handle == sentinel.file_handle, "incorrect file handle returned"
Имя модуля может быть пунктирным в форме package.module
при необходимости:
>>> @patch('package.module.ClassName.attribute', sentinel.attribute)
... def test():
... from package.module import ClassName
... assert ClassName.attribute == sentinel.attribute
...
>>> test()
Хороший паттерн - это декорирование самих методов тестирования.:
>>> class MyTest(unittest.TestCase):
... @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test_something(self):
... self.assertEqual(SomeClass.attribute, sentinel.attribute)
...
>>> original = SomeClass.attribute
>>> MyTest('test_something').test_something()
>>> assert SomeClass.attribute == original
При необходимости исправления с помощью Mock можно использовать patch()
только с одним аргументом (или patch.object()
с двумя аргументами). Для вас будет
создан mock, который будет передан в тестовую функцию/метод:
>>> class MyTest(unittest.TestCase):
... @patch.object(SomeClass, 'static_method')
... def test_something(self, mock_method):
... SomeClass.static_method()
... mock_method.assert_called_with()
...
>>> MyTest('test_something').test_something()
Вы можете сложить несколько декораторов патчей, используя этот шаблон:
>>> class MyTest(unittest.TestCase):
... @patch('package.module.ClassName1')
... @patch('package.module.ClassName2')
... def test_something(self, MockClass2, MockClass1):
... self.assertIs(package.module.ClassName1, MockClass1)
... self.assertIs(package.module.ClassName2, MockClass2)
...
>>> MyTest('test_something').test_something()
При вложении декораторов патчей моков передаются в декорированную функцию
в том же порядке, в котором они применялись (нормальный порядок Python, в
котором декораторы применяются). Это означает, что снизу вверх, так что в
приведенном выше примере mock для test_module.ClassName2
проходит сначала.
Существует также patch.dict()
установки значения в словаре непосредственно во
время область видимости и восстановления исходного состояние словаря по окончании
теста:
>>> foo = {'key': 'value'}
>>> original = foo.copy()
>>> with patch.dict(foo, {'newkey': 'newvalue'}, clear=True):
... assert foo == {'newkey': 'newvalue'}
...
>>> assert foo == original
patch
, patch.object
и patch.dict
все могут быть используемый как менеджеры
контекст.
При использовании patch()
для создания mock можно получить ссылку на
mock с помощью формы «as» инструкции:
>>> class ProductionClass:
... def method(self):
... pass
...
>>> with patch.object(ProductionClass, 'method') as mock_method:
... mock_method.return_value = None
... real = ProductionClass()
... real.method(1, 2, 3)
...
>>> mock_method.assert_called_with(1, 2, 3)
В качестве альтернативного patch
patch.object
и patch.dict
можно
использовать как декораторы классов. При таком использовании это то же самое, что и применение
декоратор индивидуально для каждого метода, имя которого начинается с «test».
Другие примеры¶
Вот еще несколько примеров для некоторых несколько более продвинутых сценариев.
Мокинг цепочечных вызовов¶
Мокинг цепочечных вызовов на самом деле просто с mock, как только вы
понимаете return_value
атрибут. При первом вызове mock или получении
его return_value
перед вызовом создается новый Mock
.
Это означает, что можно увидеть, как объект, возвращенный из вызова
mocked объекта, был используемый путем опроса return_value
mock:
>>> mock = Mock()
>>> mock().foo(a=2, b=3)
<Mock name='mock().foo()' id='...'>
>>> mock.return_value.foo.assert_called_with(a=2, b=3)
Отсюда простой шаг для настройки и последующего утверждения о цепочечных вызовов. Конечно, другой альтернативой является написание кода на более тестируемый способ в первую очередь …
Предположим, у нас есть некоторые код, которые выглядят немного так:
>>> class Something:
... def __init__(self):
... self.backend = BackendProvider()
... def method(self):
... response = self.backend.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
... # больше кода
Если предположить, что BackendProvider
уже хорошо протестирована, как мы проверяем
method()
? В частности, мы хотим проверить, что раздел кода # больше кода
использует объект ответа правильным образом.
Так как эта цепочка вызовов производится из атрибута сущности мы можем
обезьяний патчинг backend
атрибут на Something
сущность. В этом
конкретном случае нас интересует только возвращает значение от последнего
звонка в start_call
, поэтому у нас нет большой конфигурации. Предположим, что
объект, который он возвращает, является «файлообразным», поэтому убедитесь, что
наш объект ответа использует open()
builtin в качестве своего spec
.
Для этого создается mock сущность в качестве mock бэкэнда и для
него создается объект ответа mock. Чтобы установить ответ в качестве
возвращает значение для этого окончательного start_call
мы могли бы сделать
это:
mock_backend.get_endpoint.return_value.create_call.return_value.start_call.return_value = mock_response
Мы можем сделать это несколько лучше, используя метод configure_mock()
, чтобы
непосредственно установить возвращает значение для нас:
>>> something = Something()
>>> mock_response = Mock(spec=open)
>>> mock_backend = Mock()
>>> config = {'get_endpoint.return_value.create_call.return_value.start_call.return_value': mock_response}
>>> mock_backend.configure_mock(**config)
С этими мы обезьяний патчинг «mock бэкенд» на месте и может сделать реальный вызов:
>>> something.backend = mock_backend
>>> something.method()
С помощью mock_calls
мы можем проверить цепочечный вызов с одним утверждением.
Цепочечный вызов - это несколько вызовов в одной строке кода, поэтому в
mock_calls
будет несколько записей. Мы можем использовать call.call_list()
для
создания этого списка вызовов для нас:
>>> chained = call.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
>>> call_list = chained.call_list()
>>> assert mock_backend.mock_calls == call_list
Частичный мокинг¶
В некоторых тестах я хотел mock вызов datetime.date.today()
, чтобы возвращает
известную дату, но я не хотел мешать тестируемому код создавать новые
объекты даты. К сожалению, datetime.date
написан на языке C, и поэтому я не мог
выполнить простой обезьяний патчинг статического метода date.today()
.
Я нашел простой способ сделать это, который предполагал эффективную упаковку класса даты с mock, но передачу вызовов конструктора в реальный класс (и возврат реального сущности).
Здесь используемый декоратор patch
для mock класса date
в тестируемом
модуле. Затем side_effect
атрибут в классе дат mock устанавливается в
лямбда-функцию, которая возвращает реальную дату. Когда класс даты mock
называется, реальная дата будет создана и возвращенный side_effect
.:
>>> from datetime import date
>>> with patch('mymodule.date') as mock_date:
... mock_date.today.return_value = date(2010, 10, 8)
... mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
...
... assert mymodule.date.today() == date(2010, 10, 8)
... assert mymodule.date(2009, 6, 8) == date(2009, 6, 8)
Обратите внимание, что мы не патчим datetime.date
глобально, мы исправляем
date
в модуле, который его используют. Смотрите где патчить.
При вызове date.today()
возвращенный известная дата, но вызовы конструктора
date(...)
по-прежнему возвращает обычные даты. Без этого вы можете рассчитывать
ожидаемый результат по тому же алгоритму, что и тестируемый код, который
является классическим тестовым антипаттерном.
Вызовы конструктора даты записываются в mock_date
атрибуты (call_count
и
друзья), что также может быть полезно для тестов.
Альтернативный способ работы с датами мокинг или другими классами строительства обсуждается в эта запись в блоге.
Мокинг генератор метода¶
Python генератор - это функция или метод, использующий yield
инструкция для возвращает серии значения при итерации по [1].
Метод/функция генератор вызывается для возвращает объекта генератор. Это
генератор объект, который затем итерируется. Метод протокола для итерации
является __iter__()
, поэтому мы можем mock это с помощью MagicMock
.
Вот пример класса с методом «iter», реализованным как генератор:
>>> class Foo:
... def iter(self):
... for i in [1, 2, 3]:
... yield i
...
>>> foo = Foo()
>>> list(foo.iter())
[1, 2, 3]
Как бы мы mock этот класс, и в частности его метод «iter»?
Для конфигурирования значения возвращенный из итерации (неявно в вызове
list
) необходимо сконфигурировать объект, возвращенный вызовом
foo.iter()
.
>>> mock_foo = MagicMock()
>>> mock_foo.iter.return_value = iter([1, 2, 3])
>>> list(mock_foo.iter())
[1, 2, 3]
[1] | Есть также выражения генератора и многое другое расширенное использование генераторов, но мы не беспокоимся о них здесь. Очень хорошее введение в генераторы и их мощь: Трюки с генераторами для системных программистов. |
Применение одного и того же патча к каждому методу тестирования¶
Если требуется наличие нескольких патчей для нескольких методов
тестирования, то очевидным способом является применение декораторов патчей
к каждому методу. Это может чувствовать себя ненужным повторением. Для
Python 2.6 или более поздних можно использовать patch()
(во всех его
различных формах) в качестве декоратора класса. При этом патчи применяются ко
всем методам тестирования в классе. Метод тестирования определяется методами,
имена которых начинаются с test
:
>>> @patch('mymodule.SomeClass')
... class MyTest(unittest.TestCase):
...
... def test_one(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def test_two(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def not_a_test(self):
... return 'something'
...
>>> MyTest('test_one').test_one()
>>> MyTest('test_two').test_two()
>>> MyTest('test_two').not_a_test()
'something'
Альтернативным способом управления исправлениями является использование
Методы patch: start и stop. Они позволяют перемещать патч в методы setUp
и
tearDown
.:
>>> class MyTest(unittest.TestCase):
... def setUp(self):
... self.patcher = patch('mymodule.foo')
... self.mock_foo = self.patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
... def tearDown(self):
... self.patcher.stop()
...
>>> MyTest('test_foo').run()
При использовании этого метода необходимо убедиться, что патчинг «отменен»
путем вызова stop
. Это может быть более точным, чем вы думаете, потому
что если в настройке Up возникает исключение, то tearDown не вызывается.
unittest.TestCase.addCleanup()
облегчает это:
>>> class MyTest(unittest.TestCase):
... def setUp(self):
... patcher = patch('mymodule.foo')
... self.addCleanup(patcher.stop)
... self.mock_foo = patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
>>> MyTest('test_foo').run()
Мокинг несвязанных методов¶
Во время написания тестов сегодня мне нужно было исправить несвязанный метод (патч
метода в классе, а не в экземпляре). Мне нужно было передать
self в качестве первого аргумента, потому что я хочу сделать утверждения о том,
какие объекты вызывались этим методом. Проблема в том, что вы не можете
патчить mock для этого, потому что если вы заменяете несвязанный метод
на mock, он не становится связанным методом при извлечении из
сущность, и поэтому он не получает себя. Обходной путь состоит в исправлении
несвязанного метода действительной функцией. Декоратор patch()
делает так
просто латать методы с mock, что необходимость создания реальной функции
становится неприятностью.
Если передать autospec=True
в патч, то патч будет выполнен с
помощью объекта функции real. Этот объект функции имеет ту же сигнатуру,
что и заменяемый объект, но делегирует mock под колпаком. Автоматическое
создание mock по-прежнему выполняется точно так же, как и раньше. Однако
это означает, что если вы используете его для исправления несвязанного метода в
классе, то мок функция будет превращена в связанный метод, если он
извлекается из сущность. Это self
войдет в качестве первого аргумента,
что именно то, что я хотел:
>>> class Foo:
... def foo(self):
... pass
...
>>> with patch.object(Foo, 'foo', autospec=True) as mock_foo:
... mock_foo.return_value = 'foo'
... foo = Foo()
... foo.foo()
...
'foo'
>>> mock_foo.assert_called_once_with(foo)
Если мы не используем autospec=True
то несвязанный метод исправляется с помощью
сущность Mock и не вызывается с помощью self
.
Проверка нескольких вызовов с помощью mock¶
mock имеет хороший API для утверждения о том, как ваши mock объекты используются.
>>> mock = Mock()
>>> mock.foo_bar.return_value = None
>>> mock.foo_bar('baz', spam='eggs')
>>> mock.foo_bar.assert_called_with('baz', spam='eggs')
Если mock вызывается только после использования метода assert_called_once_with()
,
который также утверждает, что call_count
является единицей.
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
>>> mock.foo_bar()
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
Traceback (most recent call last):
...
AssertionError: Expected to be called once. Called 2 times.
И assert_called_with
, и assert_called_once_with
делают утверждения о самом последнем вызове.
Если ваш mock будет вызываться несколько раз, и вы хотите сделать утверждение о
всех этих вызовов, вы можете использовать call_args_list
:
>>> mock = Mock(return_value=None)
>>> mock(1, 2, 3)
>>> mock(4, 5, 6)
>>> mock()
>>> mock.call_args_list
[call(1, 2, 3), call(4, 5, 6), call()]
Помощник call
позволяет легко утверждать об этих вызовах. Можно создать
список ожидаемых вызовов и сравнить его с call_args_list
. Это выглядит удивительно
похоже на repr call_args_list
:
>>> expected = [call(1, 2, 3), call(4, 5, 6), call()]
>>> mock.call_args_list == expected
True
Справляться с изменяемыми аргументами¶
Другая ситуация редкая, но может укусить вас, это когда ваш mock
вызывается с изменяемыми аргументами. call_args
и call_args_list
хранят ссылки
к аргументам. Если аргументы мутированы тестируемым кодом, то больше
нельзя делать утверждения о том, какими были значения при вызове mock.
Вот пример код, который показывает проблему. Представьте следующие функции, определенные в «mymodule»:
def frob(val):
pass
def grob(val):
"First frob and then clear val"
frob(val)
val.clear()
Когда мы пытаемся проверить, что grob
вызывает frob
с правильным
аргументом посмотрите, что происходит:
>>> with patch('mymodule.frob') as mock_frob:
... val = {6}
... mymodule.grob(val)
...
>>> val
set()
>>> mock_frob.assert_called_with({6})
Traceback (most recent call last):
...
AssertionError: Expected: (({6},), {})
Called with: ((set(),), {})
Одна из возможностей будет для mock копировать аргументы, которые вы передаете. Это может вызвать проблемы, если вы делаете утверждения, которые полагаются на идентичность объекта для равенства.
Вот одно решение, в котором используются side_effect
функциональные возможности.
Если предусмотрена mock функция side_effect
, то side_effect
будет
вызываться с теми же аргументами, что и mock. Это дает нам возможность
скопировать аргументы и сохранить их для последующих утверждений. В этом примере
я использую другой mock для хранения аргументов, чтобы использовать
mock методы для выполнения утверждения. Снова функция помощника
настраивает это для меня:
>>> from copy import deepcopy
>>> from unittest.mock import Mock, patch, DEFAULT
>>> def copy_call_args(mock):
... new_mock = Mock()
... def side_effect(*args, **kwargs):
... args = deepcopy(args)
... kwargs = deepcopy(kwargs)
... new_mock(*args, **kwargs)
... return DEFAULT
... mock.side_effect = side_effect
... return new_mock
...
>>> with patch('mymodule.frob') as mock_frob:
... new_mock = copy_call_args(mock_frob)
... val = {6}
... mymodule.grob(val)
...
>>> new_mock.assert_called_with({6})
>>> new_mock.call_args
call({6})
copy_call_args
вызывается с вызываемым mock. Она возвращает новый
mock, на который мы делаем утверждение. Функция side_effect
создает копию
аргументов и вызывает наш new_mock
с копией.
Примечание
Если ваши mock будут используемый только после того, как появится более
простой способ проверки аргументов в момент их вызова. Можно просто выполнить
проверку внутри функции side_effect
.
>>> def side_effect(arg):
... assert arg == {6}
...
>>> mock = Mock(side_effect=side_effect)
>>> mock({6})
>>> mock(set())
Traceback (most recent call last):
...
AssertionError
Альтернативный подход заключается в создании подкласса Mock
или
MagicMock
, копирующих (с помощью copy.deepcopy()
) аргументы. Вот пример
реализации:
>>> from copy import deepcopy
>>> class CopyingMock(MagicMock):
... def __call__(self, /, *args, **kwargs):
... args = deepcopy(args)
... kwargs = deepcopy(kwargs)
... return super(CopyingMock, self).__call__(*args, **kwargs)
...
>>> c = CopyingMock(return_value=None)
>>> arg = set()
>>> c(arg)
>>> arg.add(1)
>>> c.assert_called_with(set())
>>> c.assert_called_with(arg)
Traceback (most recent call last):
...
AssertionError: Expected call: mock({1})
Actual call: mock(set())
>>> c.foo
<CopyingMock name='mock.foo' id='...'>
Когда ваш подкласс Mock
или MagicMock
, все динамично создали
атрибуты и return_value
, будете использовать свой подкласс автоматически.
Это означает, что все нижестоящие элементы CopyingMock
также будут иметь тип
CopyingMock
.
Вложение патчей¶
Использование патча в качестве менеджера контекст удобно, но если вы делаете несколько патчей, вы можете в конечном итоге вложить с отступом инструкции дальше и дальше вправо:
>>> class MyTest(unittest.TestCase):
...
... def test_foo(self):
... with patch('mymodule.Foo') as mock_foo:
... with patch('mymodule.Bar') as mock_bar:
... with patch('mymodule.Spam') as mock_spam:
... assert mymodule.Foo is mock_foo
... assert mymodule.Bar is mock_bar
... assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').test_foo()
>>> assert mymodule.Foo is original
С unittest cleanup
функций и Методы patch: start и stop мы можем достичь того же эффекта
без вложенного отступа. Простой метод помощника, create_patch
, ставит патч на
место и возвращает созданный mock для нас:
>>> class MyTest(unittest.TestCase):
...
... def create_patch(self, name):
... patcher = patch(name)
... thing = patcher.start()
... self.addCleanup(patcher.stop)
... return thing
...
... def test_foo(self):
... mock_foo = self.create_patch('mymodule.Foo')
... mock_bar = self.create_patch('mymodule.Bar')
... mock_spam = self.create_patch('mymodule.Spam')
...
... assert mymodule.Foo is mock_foo
... assert mymodule.Bar is mock_bar
... assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').run()
>>> assert mymodule.Foo is original
Мокинг словаря с MagicMock¶
Возможно, потребуется mock словарь или другой объект-контейнер, записывающий весь доступ к нему, пока он ведет себя как словарь.
Мы можем сделать это с MagicMock
, который будет вести себя как словарь, и с
помощью side_effect
делегировать словарный доступ к реальному базовому словарю,
который находится под нашим контролем.
При вызове методов __getitem__()
и __setitem__()
нашего MagicMock
(обычный доступ
к словарю) вызывается side_effect
с ключом (и в случае __setitem__
значение
тоже). Мы также можем контролировать то, что возвращенный.
После того, как MagicMock
был используемый, который мы можем использовать
атрибуты как call_args_list
, чтобы утверждать о том, как словарь был
используемый:
>>> my_dict = {'a': 1, 'b': 2, 'c': 3}
>>> def getitem(name):
... return my_dict[name]
...
>>> def setitem(name, val):
... my_dict[name] = val
...
>>> mock = MagicMock()
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem
Примечание
Альтернативой использованию MagicMock
является использование Mock
и
только предоставить волшебные методы, которые вы конкретно хотите:
>>> mock = Mock()
>>> mock.__getitem__ = Mock(side_effect=getitem)
>>> mock.__setitem__ = Mock(side_effect=setitem)
Вариант third - использовать MagicMock
, но передавать dict
в
качестве аргумента spec (или spec_set) так, чтобы создаваемый
MagicMock
имел только доступные методы словарной магии:
>>> mock = MagicMock(spec_set=dict)
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem
При наличии этих функций побочных эффектов mock
будет вести себя как
обычный словарь, но записывать доступ. Это даже вызывает KeyError
, если вы
пытаетесь получить доступ к несуществующему ключу.
>>> mock['a']
1
>>> mock['c']
3
>>> mock['d']
Traceback (most recent call last):
...
KeyError: 'd'
>>> mock['b'] = 'fish'
>>> mock['d'] = 'eggs'
>>> mock['b']
'fish'
>>> mock['d']
'eggs'
После этого используемый вы можете сделать утверждения о доступе с помощью обычных методов и атрибутов mock:
>>> mock.__getitem__.call_args_list
[call('a'), call('c'), call('d'), call('b'), call('d')]
>>> mock.__setitem__.call_args_list
[call('b', 'fish'), call('d', 'eggs')]
>>> my_dict
{'a': 1, 'b': 'fish', 'c': 3, 'd': 'eggs'}
Мок подклассы и их атрибуты¶
Есть несколько причин, по которым вы можете создать подкласс Mock
. Одной из
причин может быть добавление вспомогательных методов. Вот глупый пример:
>>> class MyMock(MagicMock):
... def has_been_called(self):
... return self.called
...
>>> mymock = MyMock(return_value=None)
>>> mymock
<MyMock id='...'>
>>> mymock.has_been_called()
False
>>> mymock()
>>> mymock.has_been_called()
True
Стандартное поведение для Mock
сущности заключается в том, что
атрибуты и возвращает значение моки имеют тот же тип, что и
mock, к которым они обращаются. Это гарантирует, что Mock
атрибуты - Mocks
, и MagicMock
атрибуты MagicMocks
[2]. Так что если
вы подклассифицируете, чтобы добавить вспомогательные методы, то они также будут
доступны на атрибуты и возвращает значение mock сущности вашего
подкласса.
>>> mymock.foo
<MyMock name='mock.foo' id='...'>
>>> mymock.foo.has_been_called()
False
>>> mymock.foo()
<MyMock name='mock.foo()' id='...'>
>>> mymock.foo.has_been_called()
True
Иногда это неудобно. Например, один пользователь выполняет подкласс mock для создания Twisted адаптера. Применение этого параметра к атрибуты также фактически приводит к ошибкам.
Mock
(во всех своих вкусах) использует метод под названием _get_child_mock
для создания этих «суб-моки» для атрибуты и возвращает значения.
Этот метод позволяет предотвратить подкласс используемых атрибутов. Это
сигнатура, что он принимает произвольные ключевые аргументы (**kwargs
),
которые затем передаются в конструктор mock:
>>> class Subclass(MagicMock):
... def _get_child_mock(self, /, **kwargs):
... return MagicMock(**kwargs)
...
>>> mymock = Subclass()
>>> mymock.foo
<MagicMock name='mock.foo' id='...'>
>>> assert isinstance(mymock, Subclass)
>>> assert not isinstance(mymock.foo, Subclass)
>>> assert not isinstance(mymock(), Subclass)
[2] | Исключением из этого правила являются не вызываемые моки. Атрибуты используют вызываемый вариант, поскольку в противном случае не вызываемые моки не могли бы иметь вызываемые методы. |
Мокинг импорта с patch.dict¶
Одна из ситуаций, когда мокинг может быть труден, это когда у вас есть локальный импорт внутри функции. Их труднее мокать, поскольку они не используют объект из пространства имен модуля, которое можно исправить.
Как правило, следует избегать локального импорта. Иногда они делаются для предотвращения циклических зависимостей, для которых обычно существует гораздо лучшего способа решения проблемы (рефакторить код) или для предотвращения «начальных затрат» путём задержки импорта. Это также можно решить лучше, чем безусловный импорт локальная (сохраните модуль как класс или модуль атрибут и импортируйте только при первом использовании).
Кроме того, существует способ использовать mock
для воздействия на
результаты импорта. При импорте объекта извлекается из sys.modules
словаря.
Обратите внимание, что он получает объект, который не обязательно должен
быть модулем. При первом импорте модуля объект модуля помещается в
«sys.modules», поэтому обычно при импорте чего-либо модуль возвращается обратно.
Однако это не обязательно.
Это означает, что patch.dict()
можно использовать для временно установки
mock в sys.modules
. Любой импорт, пока этот патч активен, извлекает
mock. Когда патч завершен (декорированная функция выходит, тело с
инструкция завершено или patcher.stop()
называется), то все, что было ранее, будет
восстановлено безопасно.
Вот пример, который моки модуля fooble.
>>> import sys
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
... import fooble
... fooble.blob()
...
<Mock name='mock.blob()' id='...'>
>>> assert 'fooble' not in sys.modules
>>> mock.blob.assert_called_once_with()
Как видите, import fooble
удаётся, но на выходе в sys.modules
не осталось
«фальшивки».
Это также работает для from module import name
формы:
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
... from fooble import blob
... blob.blip()
...
<Mock name='mock.blob.blip()' id='...'>
>>> mock.blob.blip.assert_called_once_with()
Немного больше работы можно также mock импорт пакетов:
>>> mock = Mock()
>>> modules = {'package': mock, 'package.module': mock.module}
>>> with patch.dict('sys.modules', modules):
... from package.module import fooble
... fooble()
...
<Mock name='mock.module.fooble()' id='...'>
>>> mock.module.fooble.assert_called_once_with()
Отслеживание порядка вызовов и менее подробных утверждений о вызовах¶
Класс Mock
позволяет отслеживать порядок вызовов методов на объектах
mock через method_calls
атрибут. Это не позволяет отслеживать порядок
вызовов между отдельными объектами mock, однако мы можем использовать
mock_calls
для достижения того же эффекта.
Поскольку моки отслеживаем вызовы дочерних моков в mock_calls
, а
доступ к произвольному атрибут mock создает дочерний mock, мы
можем создать наш отдельный моки из родительского. Вызовы этих дочерних
mock будут записываться по порядку в mock_calls
родителя:
>>> manager = Mock()
>>> mock_foo = manager.foo
>>> mock_bar = manager.bar
>>> mock_foo.something()
<Mock name='mock.foo.something()' id='...'>
>>> mock_bar.other.thing()
<Mock name='mock.bar.other.thing()' id='...'>
>>> manager.mock_calls
[call.foo.something(), call.bar.other.thing()]
>>> менеджер mock_calls [call.foo.something (), call.bar.other.thing ()]
Затем мы можем утверждать о вызовах, включая заказ, сравнивая с mock_calls
атрибут на mock: менеджера
>>> expected_calls = [call.foo.something(), call.bar.other.thing()]
>>> manager.mock_calls == expected_calls
True
Если patch
создает и размещает моки, то их можно присоединить к
mock менеджера с помощью метода attach_mock()
. После присоединения вызовы
записываются в mock_calls
менеджера.:
>>> manager = MagicMock()
>>> with patch('mymodule.Class1') as MockClass1:
... with patch('mymodule.Class2') as MockClass2:
... manager.attach_mock(MockClass1, 'MockClass1')
... manager.attach_mock(MockClass2, 'MockClass2')
... MockClass1().foo()
... MockClass2().bar()
<MagicMock name='mock.MockClass1().foo()' id='...'>
<MagicMock name='mock.MockClass2().bar()' id='...'>
>>> manager.mock_calls
[call.MockClass1(),
call.MockClass1().foo(),
call.MockClass2(),
call.MockClass2().bar()]
Если было сделано много вызовов, но вы заинтересованы только в определенной
последовательности из них, то альтернативой является использование метода
assert_has_calls()
. При этом берется список вызовов (созданных с помощью объекта
call
). Если эта последовательность вызовов находится в mock_calls
, то
утверждение выполняется успешно.
>>> m = MagicMock()
>>> m().foo().bar().baz()
<MagicMock name='mock().foo().bar().baz()' id='...'>
>>> m.one().two().three()
<MagicMock name='mock.one().two().three()' id='...'>
>>> calls = call.one().two().three().call_list()
>>> m.assert_has_calls(calls)
Несмотря на то, что цепочка m.one().two().three()
вызовов не являются единственными
вызовами, которые были сделаны в mock, утверждение по-прежнему успешно.
Иногда в mock может сделать несколько вызовов, и вы заинтересованы
только в утверждении о некоторые из этих вызовов. Вы можете даже не заботиться
о порядке. В этом случае можно передать any_order=True
assert_has_calls
:
>>> m = MagicMock()
>>> m(1), m.two(2, 3), m.seven(7), m.fifty('50')
(...)
>>> calls = [call.fifty('50'), call(1), call.seven(7)]
>>> m.assert_has_calls(calls, any_order=True)
Более сложное сопоставление аргументов¶
Используя ту же самую базовую концепцию, что и ANY
, мы можем реализовать
средства согласования для выполнения более сложных утверждений об объектах,
используемый как аргументы для моки.
Предположим, что какой-либо объект будет передан в mock, который по
умолчанию сравнивает равные значения на основе идентификатора объекта (который
является Python значением по умолчанию для определяемых пользователем
классов). Чтобы использовать assert_called_with()
, нам нужно передать один и тот же
объект. Если нас интересуют только некоторые атрибуты этого объекта, то мы
можем создать сопоставителя, который проверит эти атрибуты для нас.
В этом примере показано, как «стандартного» вызова assert_called_with
недостаточно:
>>> class Foo:
... def __init__(self, a, b):
... self.a, self.b = a, b
...
>>> mock = Mock(return_value=None)
>>> mock(Foo(1, 2))
>>> mock.assert_called_with(Foo(1, 2))
Traceback (most recent call last):
...
AssertionError: Expected: call(<__main__.Foo object at 0x...>)
Actual call: call(<__main__.Foo object at 0x...>)
Функция сравнения для нашего класса Foo
может выглядеть примерно так:
>>> def compare(self, other):
... if not type(self) == type(other):
... return False
... if self.a != other.a:
... return False
... if self.b != other.b:
... return False
... return True
...
Объект сравнения, который может использовать такие функции сравнения для своей операции равенства, будет выглядеть примерно так:
>>> class Matcher:
... def __init__(self, compare, some_obj):
... self.compare = compare
... self.some_obj = some_obj
... def __eq__(self, other):
... return self.compare(self.some_obj, other)
...
Собрать все это вместе:
>>> match_foo = Matcher(compare, Foo(1, 2))
>>> mock.assert_called_with(match_foo)
Экземпляр Matcher
создается с помощью функции сравнения и объекта
Foo
, с которым требуется сравнить. В assert_called_with
будет вызван метод
Matcher
равенства, который сравнивает объект, с которым был вызван
mock, с тем, с которым мы создали нашего спичера. Если они совпадают, то
assert_called_with
проходит, а если они не AssertionError
поднимается:
>>> match_wrong = Matcher(compare, Foo(3, 4))
>>> mock.assert_called_with(match_wrong)
Traceback (most recent call last):
...
AssertionError: Expected: ((<Matcher object at 0x...>,), {})
Called with: ((<Foo object at 0x...>,), {})
С помощью немного настройки вы можете иметь функцию сравнения поднять
AssertionError
непосредственно и предоставить более полезное сообщение о сбое.
Начиная с версии 1.5, библиотека PyHamcrest тестирования Python предоставляет аналогичные функциональные возможности, которые могут быть полезны здесь, в виде своего устройства согласования равенства (hamcrest.library.integration.match_equality).