Добавление фиктивных объектов в Python
Мой тестируемый код выглядит так:
def to_be_tested(x):
return round((x.a + x.b).c())
Я хотел бы проверить это, передав объект Mock как x. Я пытался сделать это так:
import unittest
import unittest.mock
class Test_X(unittest.TestCase):
def test_x(self):
m = unittest.mock.Mock()
to_be_tested(m)
# now check if the proper call has taken place on m
Призыв к x.a
и тот, чтобы x.b
работать как положено. Они доставляют новые фиктивные объекты, которые можно спросить, как они были созданы (например, через q._mock_parent
а также q._mock_new_name
), так что этот шаг работает просто отлично.
Но тогда должно произойти сложение, которое просто вызывает ошибку (TypeError: unsupported operand type(s) for +: 'Mock' and 'Mock'
). Я надеялся, что это также вернет фиктивный объект, так что вызов .c()
может иметь место и (снова) вернуть фиктивный объект.
Я тоже считала m.__add__ = lambda a, b: unittest.mock.Mock(a, b)
до вызова тестируемого кода, но это не поможет, так как будет добавлен не мой оригинальный Mock, а только что созданный.
Я тоже попробовал (уже довольно громоздко) m.a.__add__ = lambda a, b: unittest.mock.Mock(a, b)
, Но это (к моему удивлению) привело к повышению AttributeError: Mock object has no attribute 'c'
при вызове тестируемого кода. Что я не понимаю, потому что Mock, который я создаю там, должен принять то, что я назвал c()
в этом, верно?
Есть ли способ добиться того, чего я хочу? Как я могу создать макет, который можно добавить в другой макет?
Или есть другой стандартный способ тестирования кода, подобного моему?
РЕДАКТИРОВАТЬ: я не заинтересован в предоставлении специализированного кода, который готовит пропущенные макеты для ожидаемых вызовов. Я только хочу после звонка проверить, что все происходило, как ожидалось, проверяя переданные и возвращенные фиктивные объекты. Я думаю, что этот путь должен быть возможным, и в этом (и в других сложных случаях, подобных этому) я мог бы использовать его.
2 ответа
Добавление объектов требует, чтобы эти объекты по крайней мере реализовали __add__
специальный метод, называемый магическими методами Mock, см. раздел " Методы магической магии " в документации:
Поскольку магические методы выглядят иначе, чем обычные методы, эта поддержка была специально реализована. Это означает, что поддерживаются только определенные магические методы. Список поддерживаемых включает в себя почти все из них. Если вам что-то не хватает, пожалуйста, сообщите нам.
Самый простой способ получить доступ к тем магическим методам, которые поддерживаются mock
, вы можете создать экземпляр MagicMock
класс, который обеспечивает реализации по умолчанию для тех (каждый возвращает сейчас MagicMock
экземпляр по умолчанию).
Это дает вам доступ к x.a + x.b
вызов:
>>> from unittest import mock
>>> m = mock.MagicMock()
>>> m.a + m.b
<MagicMock name='mock.a.__add__()' id='4500141448'>
>>> m.mock_calls
[call.a.__add__(<MagicMock name='mock.b' id='4500112160'>)]
Вызов m.a.__add__()
был записан с аргументом m.b
; это то, что мы можем теперь утверждать в тесте!
Далее то же самое m.a.__add__()
макет затем используется для снабжения .c()
насмешка:
>>> (m.a + m.b).c()
<MagicMock name='mock.a.__add__().c()' id='4500162544'>
Опять же, это то, что мы можем утверждать. Обратите внимание, что если вы повторите этот вызов, вы обнаружите, что макеты являются синглетонами; при доступе к атрибутам или вызове макета создается и сохраняется больше макетов одного и того же типа, вы можете позже использовать эти сохраненные объекты, чтобы утверждать, что нужный объект был выдан; Вы можете достичь результата звонка с Mock.return_value
атрибут:
>>> m.a.__add__.return_value.c.return_value
<MagicMock name='mock.a.__add__().c()' id='4500162544'>
>>> (m.a + m.b).c() is m.a.__add__.return_value.c.return_value
True
Теперь, чтобы round()
, round()
также вызывает магический метод, __round__()
метод. К сожалению, этого нет в списке поддерживаемых методов:
>>> round(mock.MagicMock())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: type MagicMock doesn't define __round__ method
Это, вероятно, упущение, так как другие числовые методы, такие как __trunc__
а также __ceil__
включены. Я отправил отчет об ошибке, чтобы запросить его добавление. Вы можете вручную добавить это к MagicMock
Список поддерживаемых методов с:
mock._magics.add('__round__') # set of magic methods MagicMock supports
_magics
это набор; добавление __round__
когда он уже существует в этом наборе, он безвреден, поэтому вышеизложенное является перспективным. Альтернативный обходной путь - издеваться над round()
встроенная функция, используя mock.patch()
установить новый round
глобальный в модуле, где находится ваша тестируемая функция.
Далее при тестировании у вас есть 3 варианта:
Проведите тестирование, установив возвращаемые значения для вызовов, включая типы, отличные от ложных. Например, вы можете настроить макет так, чтобы он возвращал значение с плавающей запятой для
.c()
позвоните, чтобы вы могли утверждать, что вы получите правильно округленные результаты:>>> m.a.__add__.return_value.c.return_value = 42.12 # (m.a + ??).c() returns 42.12 >>> round((m.a + m.b).c()) == 42 True
Утверждают, что конкретные звонки имели место. Есть целая серия
assert_call*
методы, которые помогут вам с проверкой вызова, всех вызовов, вызовов в определенном порядке и т. д. Есть также такие атрибуты, как.called
,.call_count
, а такжеmock_calls
, Проверьте это.Утверждая что
m.a + m.b
произошло означает, чтоm.a.__add__
был вызван сm.b
в качестве аргумента:>>> m = mock.MagicMock() >>> m.a + m.b <MagicMock name='mock.a.__add__()' id='4500337776'> >>> m.a.__add__.assert_called_with(m.b) # returns None, so success
Если вы хотите проверить
Mock
возвращаем значение экземпляра, переходим к ожидаемому фиктивному объекту и используемis
проверить на личность:>>> mock._magics.add('__round__') >>> m = mock.MagicMock() >>> r = round((m.a + m.b).c()) >>> mock_c_result = m.a.__add__.return_value.c.return_value >>> r is mock_c_result.__round__.return_value True
Никогда не нужно возвращаться от ложного результата к родителям и т. Д. Просто пройдите другой путь.
Причина, по которой ваша лямбда __add__
не работает, потому что вы создали Mock()
экземпляр с аргументами. Первые два аргумента являются spec
и side_effect
аргументы. spec
Аргумент ограничивает, какие атрибуты поддерживает макет, и так как вы передали a
в качестве спецификации ложного объекта и a
объект не имеет атрибута c
, вы получаете ошибку атрибута на c
,
Я сам нашел решение, но оно не слишком красивое. Потерпите меня.
Нормальный Mock
объекты готовы записать много обработки, которую они испытывают, но не все. Например они будут записывать, когда их вызывают, когда запрашивается атрибут, и некоторые другие вещи. Однако они не будут записывать (или принимать), если они, например, добавлены друг к другу. Добавление считается "магической" операцией с использованием "магического метода" (__add__
) объектов и Mock
не поддерживают их.
Для них есть еще один класс под названием MagicMock
, MagicMock
объекты поддерживают магические методы, поэтому их добавление работает для них. Результат будет другим MagicMock
объект, который можно спросить, как он был создан (добавив два других MagicMock
объекты).
К сожалению, в текущей версии (3.6.5) магический метод __round__
(который называется когда round(o)
называется) пока не входит. Я думаю, они просто забыли перечислить, что среди других магических методов, таких как __trunc__
, __floor__
, __ceil__
и т. д. Когда я добавил его в исходные тексты, я смог правильно протестировать и мой тестируемый код, включая round()
вызов.
Но исправление установленных модулей Python, конечно, не способ сделать это. Поскольку в текущей реализации есть недостаток, который, как я ожидаю, будет исправлен в будущем, мое текущее решение состоит в том, чтобы изменить только внутренние структуры данных mock
модуль после его импорта.
Теперь мой тест выглядит так:
def to_be_tested(x):
return round((x.a + x.b).c())
import unittest
import unittest.mock
# patch mock module's internal data structures to support round():
unittest.mock._all_magics.add('__round__')
unittest.mock._magics.add('__round__')
class Test_X(unittest.TestCase):
def test_x(self):
m = unittest.mock.MagicMock()
r = to_be_tested(m)
# now for the tests:
self.assertEqual(r._mock_new_name, '()') # created by calling
round_call = r._mock_new_parent
self.assertEqual(round_call._mock_new_name, '__round__')
c_result = round_call._mock_new_parent
self.assertEqual(c_result._mock_new_name, '()') # created by calling
c_call = c_result._mock_new_parent
self.assertEqual(c_call._mock_new_name, 'c')
add_result = c_call._mock_new_parent
self.assertEqual(add_result._mock_new_name, '()') # created by calling
add_call = add_result._mock_new_parent
self.assertEqual(add_call._mock_new_name, '__add__')
a_attribute = add_call._mock_new_parent
b_attribute = add_call.call_args[0][0]
self.assertEqual(a_attribute._mock_new_name, 'a')
self.assertEqual(b_attribute._mock_new_name, 'b')
self.assertIs(a_attribute._mock_new_parent, m)
self.assertIs(b_attribute._mock_new_parent, m)
Test_X().test_x()
Простой тест, как self.assertEqual(r, round((m.a + m.b).c()))
к сожалению, этого недостаточно, потому что это не проверяет имя атрибута b
(и кто знает что еще).