Правильная проверка объектов 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'>)]
    
Другие вопросы по тегам