Правильная проверка объектов MagicMock в юнит-тестах Python
У меня есть этот тестируемый код:
def to_be_tested(x):
return round((x.a + x.b).c())
В моем тесте я хочу утверждать, что именно это делается с x
и результат вернулся, поэтому я передаю MagicMock
объект как x
:
class Test_X(unittest.TestCase):
def test_x(self):
m = unittest.mock.MagicMock()
r = to_be_tested(m)
Затем я проверяю результат на то, что я ожидаю:
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)
После импорта unittest.mock
Мне нужно исправить внутреннюю структуру mock
модуль для того, чтобы иметь возможность правильно магии round()
функция (подробнее см. /questions/8440596/dobavlenie-fiktivnyih-obektov-v-python/8440613#8440613):
unittest.mock._all_magics.add('__round__')
unittest.mock._magics.add('__round__')
Итак, теперь, как я уже сказал, это работает. Но я нахожу это крайне нечитаемым. Кроме того, мне нужно было много поиграть, чтобы найти такие вещи, как _mock_new_parent
и т.д. Подчеркивание также указывает на то, что это атрибут "private", и его не следует использовать. Документация не упоминает об этом. Это также не упоминает другой способ достижения того, что я пытаюсь сделать.
Есть ли более хороший способ проверить, вернулся MagicMock
объекты для создания такими, какими они должны были быть?
1 ответ
Вы идете за борт. Вы тестируете реализацию, а не результат. Более того, вы попадаете во внутреннюю часть фиктивной реализации, к которой вам не нужно прикасаться.
Проверьте, что вы получите правильный результат, и убедитесь, что результат основан на тех данных, которые вы хотите использовать. Вы можете настроить макет так, чтобы round()
передается фактическое числовое значение для округления:
x.a + x.b
приводит к звонкуm.a.__add__
, проходя вm.b
,m.a.__add__().c()
называется, поэтому мы можем проверить, что он был вызван, если это необходимо.- Просто установите результат
c()
на номер дляround()
округлить. Получение правильногоround(number)
результат от функции означает.c()
назывался.
Переходя в число к round()
здесь достаточно, потому что вы не тестируете round()
функция Вы можете положиться на сопровождающих Python, чтобы протестировать эту функцию, сосредоточившись на тестировании своего собственного кода.
Вот что я бы протестировал:
m = unittest.mock.MagicMock()
# set a return value for (x.a + *something*).c()
mock_c = m.a.__add__.return_value.c
mock_c.return_value = 42.4
r = to_be_tested(m)
mock_c.assert_called_once()
self.assertEqual(r, 42)
Если вы должны утверждать, что m.a + m.b
состоялось, то вы можете добавить
m.a.__add__.assert_called_once(m.b)
но mock_c
передача вызова assert уже доказывает, что по крайней мере (m.a + <whatever>)
выражение имело место и что c
был получен доступ на результат.
Если вы должны подтвердить это round()
был использован на фактическом макете, вам придется придерживаться исправления MagicMock
класс для включения __round__
в качестве специального метода и удалить mock_c.return_value
присваивание, после которого вы можете утверждать, что возвращаемое значение является правильным объектом с
# assert that the result of the `.c()` call has been passed to the
# round() function (which returns the result of `.__round__()`).
self.assertIs(r, mock_c.return_value.__round__.return_value)
Некоторые дальнейшие заметки:
- Нет смысла пытаться сделать все фиктивным объектом. Если тестируемый код должен работать со стандартными типами Python, просто сделайте, чтобы ваши типы создавали эти типы. Например, если ожидается, что какой-то вызов выдаст строку, ваш макет вернет тестовую строку, особенно когда вы затем передаете материал в другие API стандартной библиотеки.
- Издевается над одиночками. Вам не нужно возвращаться из данного макета, чтобы проверить, что у них есть правильный родительский элемент, потому что вы можете добраться до того же объекта, пройдя через родительские атрибуты, а затем используя
is
, Например, если функция возвращает где-нибудь фиктивный объект, вы можете утверждать, что правильный тестовый объект был возвращен тестированиемassertIs(mock_object.some.access.return_value.path, returned_object)
, - Когда издевается, этот факт записывается. Вы можете утверждать это с
assert_called*
методы,.called
а также.call_count
атрибуты и проследить результат вызовов с.return_value
атрибуты Если вы сомневаетесь, осмотрите
.mock_calls
атрибут, чтобы увидеть, к какому тестовому коду обращались. Или сделать это в интерактивном сеансе. Например, легче увидеть, чтоm.a + m.b
делает в быстром тесте с:>>> from unittest import mock >>> m = mock.MagicMock() >>> m.a + m.b <MagicMock name='mock.a.__add__()' id='4495452648'> >>> m.mock_calls [call.a.__add__(<MagicMock name='mock.b' id='4495427568'>)]