unittest.mock
— getting started¶
Added in version 3.3.
Використання Mock¶
Імітаційні методи виправлення¶
Загальні випадки використання об’єктів Mock
включають:
Методи латання
Виклики методів запису на об’єктах
Можливо, ви захочете замінити метод на об’єкті, щоб перевірити, чи викликається він із правильними аргументами іншою частиною системи:
>>> real = SomeClass()
>>> real.method = MagicMock(name='method')
>>> real.method(3, 4, 5, key='value')
<MagicMock name='method()' id='...'>
Після того, як наш макет був використаний (real.method
у цьому прикладі), він має методи та атрибути, які дозволяють робити твердження про те, як він використовувався.
Примітка
У більшості цих прикладів класи Mock
і MagicMock
взаємозамінні. Оскільки MagicMock
є більш потужним класом, його доцільно використовувати за замовчуванням.
Після виклику макета його атрибут called
має значення True
. Що ще важливіше, ми можемо використовувати метод assert_ called_with()
або assert_ called_once_with()
, щоб перевірити, чи його було викликано з правильними аргументами.
Цей приклад перевіряє, що виклик ProductionClass().method
призводить до виклику something
методу:
>>> class ProductionClass:
... def method(self):
... self.something(1, 2, 3)
... def something(self, a, b, c):
... pass
...
>>> real = ProductionClass()
>>> real.something = MagicMock()
>>> real.method()
>>> real.something.assert_called_once_with(1, 2, 3)
Макет для викликів методів об’єкта¶
В останньому прикладі ми патчили метод безпосередньо на об’єкті, щоб перевірити, чи правильно його викликали. Іншим поширеним випадком використання є передача об’єкта в метод (або деяку частину тестованої системи), а потім перевірка його правильного використання.
Простий 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, а також до їхніх дочірніх атрибутів.
>>> mock = MagicMock()
>>> mock.method()
<MagicMock name='mock.method()' id='...'>
>>> mock.attribute.method(10, x=53)
<MagicMock name='mock.attribute.method()' id='...'>
>>> mock.mock_calls
[call.method(), call.attribute.method(10, x=53)]
Якщо ви робите твердження щодо mock_calls
і були викликані будь-які неочікувані методи, тоді твердження не вдасться. Це корисно, оскільки ви не тільки стверджуєте, що дзвінки, які ви очікували, були здійснені, але й перевіряєте, що вони були здійснені в правильному порядку та без додаткових дзвінків:
Ви використовуєте об’єкт call
для створення списків для порівняння з mock_calls
:
>>> expected = [call.method(), call.attribute.method(10, x=53)]
>>> mock.mock_calls == expected
True
Однак параметри викликів, які повертають імітації, не записуються, що означає, що неможливо відстежувати вкладені виклики, де важливі параметри, які використовуються для створення предків:
>>> m = Mock()
>>> m.factory(important=True).deliver()
<Mock name='mock.factory().deliver()' id='...'>
>>> m.mock_calls[-1] == call.factory(important=False).deliver()
True
Налаштування повернених значень і атрибутів¶
Встановлення повернених значень для макетного об’єкта тривіально легко:
>>> mock = Mock()
>>> mock.return_value = 3
>>> mock()
3
Звичайно, ви можете зробити те саме для методів на mock:
>>> mock = Mock()
>>> mock.method.return_value = 3
>>> mock.method()
3
Повернене значення також можна встановити в конструкторі:
>>> mock = Mock(return_value=3)
>>> mock()
3
Якщо вам потрібне налаштування атрибута на макеті, просто зробіть це:
>>> mock = Mock()
>>> mock.x = 3
>>> mock.x
3
Іноді потрібно змакувати складнішу ситуацію, як, наприклад, mock.connection.cursor().execute("SELECT 1")
. Якщо ми хочемо, щоб цей виклик повертав список, ми повинні налаштувати результат вкладеного виклику.
Ми можемо використовувати call
, щоб створити набір викликів у «ланцюжковому виклику», подібному до цього для легкого твердження згодом:
>>> mock = Mock()
>>> cursor = mock.connection.cursor.return_value
>>> cursor.execute.return_value = ['foo']
>>> mock.connection.cursor().execute("SELECT 1")
['foo']
>>> expected = call.connection.cursor().execute("SELECT 1").call_list()
>>> mock.mock_calls
[call.connection.cursor(), call.connection.cursor().execute('SELECT 1')]
>>> mock.mock_calls == expected
True
Саме виклик .call_list()
перетворює наш об’єкт виклику на список викликів, що представляють ланцюгові виклики.
Створення винятків за допомогою моків¶
Корисним атрибутом є side_effect
. Якщо ви встановите це як клас винятку або екземпляр, тоді виняток буде викликано під час виклику макета.
>>> mock = Mock(side_effect=Exception('Boom!'))
>>> mock()
Traceback (most recent call last):
...
Exception: Boom!
Функції побічних ефектів та ітерації¶
side_effect
також може бути встановлено як функція або iterable. Варіант використання side_effect
як ітерації полягає в тому, що ваш макет буде викликатися кілька разів, і ви хочете, щоб кожен виклик повертав інше значення. Коли ви встановлюєте side_effect
на iterable, кожен виклик mock повертає наступне значення з iterable:
>>> 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 є те, що воно поєднує ваші тести з реалізацією ваших mocks, а не з вашим реальним кодом. Припустімо, у вас є клас, який реалізує some_method
. У тесті для іншого класу ви надаєте макет цього об’єкта, який також надає some_method
. Якщо пізніше ви переробите перший клас, щоб він більше не мав some_method
, тоді ваші тести продовжуватимуть проходити, навіть якщо ваш код зараз зламаний!
Mock
дозволяє вам надати об’єкт як специфікацію для 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'?
Використання специфікації також дає змогу розумніше зіставляти виклики, зроблені для 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_set замість spec.
Using side_effect to return per file content¶
mock_open()
is used to patch open()
method. side_effect
can be used to return a new Mock object per call. This can be used to return different
contents per file stored in a dictionary:
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()
важливо, щоб ви латали об’єкти в просторі імен, де вони шукаються. Зазвичай це просто, але для короткого посібника прочитайте де патч.
Загальною потребою в тестах є виправлення атрибута класу або атрибута модуля, наприклад, виправлення вбудованого модуля або виправлення класу в модулі, щоб перевірити, чи створено його екземпляр. Модулі та класи фактично є глобальними, тому після тесту потрібно скасувати їх виправлення, інакше виправлення збережеться в інших тестах і спричинить проблеми, які важко діагностувати.
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
. Коли макет викликається вперше або ви отримуєте його return_value
до його виклику, створюється новий Mock
.
Це означає, що ви можете побачити, як об’єкт, повернутий викликом імітованого об’єкта, використовувався, запитуючи макет return_value
:
>>> mock = Mock()
>>> mock().foo(a=2, b=3)
<Mock name='mock().foo()' id='...'>
>>> mock.return_value.foo.assert_called_with(a=2, b=3)
Звідси це простий крок, щоб налаштувати та зробити твердження щодо ланцюжкових викликів. Звісно, іншою альтернативою є написання коду більш придатним для перевірки способом…
Отже, припустімо, що ми маємо код, який виглядає приблизно так:
>>> class Something:
... def __init__(self):
... self.backend = BackendProvider()
... def method(self):
... response = self.backend.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
... # more code
Якщо припустити, що BackendProvider
вже добре перевірено, як ми перевіримо method()
? Зокрема, ми хочемо перевірити, чи розділ коду # more code
правильно використовує об’єкт відповіді.
Оскільки цей ланцюжок викликів здійснюється з атрибута екземпляра, ми можемо виправити атрибут backend
на екземплярі Something
. У цьому конкретному випадку нас цікавить лише значення, яке повертає останній виклик start_call
, тому нам не потрібно робити багато налаштувань. Припустімо, що об’єкт, який він повертає, є «файлоподібним», тому ми переконаємося, що наш об’єкт відповіді використовує вбудований open()
як свою специфікацію
.
Для цього ми створюємо макет екземпляра як наш макет бекенда та створюємо для нього об’єкт імітації відповіді. Щоб встановити відповідь як значення, що повертається для цього останнього 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
Часткове глузування¶
In some tests I wanted to mock out a call to datetime.date.today()
to return a known date, but I didn’t want to prevent the code under test from
creating new date objects. Unfortunately datetime.date
is written in C, and
so I couldn’t just monkey-patch out the static datetime.date.today()
method.
Я знайшов простий спосіб зробити це, який передбачав ефективне обгортання класу дати макетом, але передачу викликів конструктору до реального класу (і повернення реальних екземплярів).
The patch decorator
is used here to
mock out the date
class in the module under test. The side_effect
attribute on the mock date class is then set to a lambda function that returns
a real date. When the mock date class is called a real date will be
constructed and returned by 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 і friends), що також може бути корисним для ваших тестів.
Альтернативний спосіб роботи з імітаційними датами або іншими вбудованими класами обговорюється в цьому записі блогу.
Висміювання методу генератора¶
Генератор 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]
Застосування того самого патча до кожного методу тестування¶
If you want several patches in place for multiple test methods the obvious way
is to apply the patch decorators to every method. This can feel like unnecessary
repetition. Instead, you can use patch()
(in all its
various forms) as a class decorator. This applies the patches to all test
methods on the class. A test method is identified by methods whose names start
with 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()
Висміювання незв’язаних методів¶
Сьогодні під час написання тестів мені потрібно було виправити незв’язаний метод (виправити метод у класі, а не в екземплярі). Мені потрібно було передати self як перший аргумент, оскільки я хочу зробити твердження про те, які об’єкти викликають цей конкретний метод. Проблема полягає в тому, що ви не можете виправити за допомогою макету для цього, тому що якщо ви заміните неприв’язаний метод на макет, він не стане зв’язаним методом під час отримання з примірника, і тому він не передається самостійно. Обхідним шляхом є заміна незв’язаного методу реальною функцією. Декоратор patch()
робить так простим виправлення методів за допомогою макету, що створення справжньої функції стає незручністю.
Якщо ви передаєте autospec=True
для виправлення, тоді він виконує виправлення за допомогою реального функціонального об’єкта. Цей функціональний об’єкт має таку саму сигнатуру, як і той, який він замінює, але делегує макет під капотом. Ви все ще автоматично створюєте свій макет точно так само, як і раніше. Однак це означає, що якщо ви використовуєте його для виправлення незв’язаного методу в класі, імітована функція буде перетворена на зв’язаний метод, якщо її буде отримано з екземпляра. Першим аргументом буде передано self
, а це саме те, що я хотів:
>>> class Foo:
... def foo(self):
... pass
...
>>> with patch.object(Foo, 'foo', autospec=True) as mock_foo:
... mock_foo.return_value = 'foo'
... foo = Foo()
... foo.foo()
...
'foo'
>>> mock_foo.assert_called_once_with(foo)
Якщо ми не використовуємо autospec=True
, тоді неприв’язаний метод замість цього виправляється за допомогою екземпляра Mock і не викликається за допомогою self
.
Перевірка кількох викликів за допомогою імітації¶
mock має гарний API для створення тверджень про те, як використовуються ваші макетні об’єкти.
>>> mock = Mock()
>>> mock.foo_bar.return_value = None
>>> mock.foo_bar('baz', spam='eggs')
>>> mock.foo_bar.assert_called_with('baz', spam='eggs')
If your mock is only being called once you can use the
assert_called_once_with()
method that also asserts that the
call_count
is one.
>>> 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
. Це виглядає надзвичайно схожим на відображення списку викликів_аргів
:
>>> expected = [call(1, 2, 3), call(4, 5, 6), call()]
>>> mock.call_args_list == expected
True
Робота зі змінними аргументами¶
Інша ситуація рідкісна, але може вас вкусити, коли вашу імітацію викликають зі змінними аргументами. 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(),), {})
Однією з можливостей було б імітаційне копіювання аргументів, які ви передаєте. Це може спричинити проблеми, якщо ви робите твердження, які покладаються на ідентичність об’єкта для рівності.
Here’s one solution that uses the side_effect
functionality. If you provide a side_effect
function for a mock then
side_effect
will be called with the same args as the mock. This gives us an
opportunity to copy the arguments and store them for later assertions. In this
example I’m using another mock to store the arguments so that I can use the
mock methods for doing the assertion. Again a helper function sets this up for
me.
>>> 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
разом із копією.
Примітка
Якщо ваш макет використовуватиметься лише один раз, є простіший спосіб перевірити аргументи в момент їх виклику. Ви можете просто зробити перевірку всередині функції 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
.
Патчі гніздування¶
Використовувати patch як менеджер контексту добре, але якщо ви робите кілька патчів, ви можете отримати вкладені з операторами відступи все далі й далі праворуч:
>>> 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 і методи латання: запуск і зупинка ми можемо досягти того самого ефекту без вкладеного відступу. Простий допоміжний метод, 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
для делегування доступу до словника справжньому базовому словнику, який знаходиться під нашим контролем.
When the __getitem__()
and __setitem__()
methods
of our MagicMock
are called
(normal dictionary access) then side_effect
is called with the key (and in
the case of __setitem__
the value too). We can also control what is returned.
Після використання 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
З цими функціями побічних ефектів мокет
поводитиметься як звичайний словник, але записуватиме доступ. Він навіть викликає 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
є 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
Sometimes this is inconvenient. For example, one user is subclassing mock to created a Twisted adaptor. Having this applied to attributes too actually causes errors.
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¶
Одна ситуація, коли насмішка може бути важкою, це коли у вас є локальний імпорт всередині функції. Їх важче висміяти, оскільки вони не використовують об’єкт із простору імен модуля, який ми можемо виправити.
Загалом слід уникати місцевого імпорту. Іноді їх роблять, щоб запобігти циклічним залежностям, для яких зазвичай існує набагато кращий спосіб вирішити проблему (рефакторити код) або запобігти «попереднім витратам» шляхом затримки імпорту. Це також можна вирішити кращим способом, ніж безумовний локальний імпорт (зберігайте модуль як клас або атрибут модуля та виконуйте імпорт лише під час першого використання).
That aside there is a way to use mock
to affect the results of an import.
Importing fetches an object from the sys.modules
dictionary. Note that it
fetches an object, which need not be a module. Importing a module for the
first time results in a module object being put in sys.modules
, so usually
when you import something you get a module back. This need not be the case
however.
Це означає, що ви можете використовувати 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()
Як ви бачите, імпорт 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
для досягнення того самого ефекту.
Оскільки mocks відстежують виклики дочірніх mocks у mock_calls
, а доступ до довільного атрибута 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
макет менеджера:
>>> 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, твердження все одно успішне.
Іноді макет може мати кілька звернень до нього, і ви зацікавлені лише в підтвердженні деяких із цих викликів. Ви можете навіть не дбати про порядок. У цьому випадку ви можете передати 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
...
І об’єкт відповідності, який може використовувати такі функції порівняння для своєї операції рівності, виглядатиме приблизно так:
>>> 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
, який порівнює об’єкт, з яким було викликано макет, з тим, з яким ми створили наш збігувач. Якщо вони збігаються, 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).