Правильная проверка объектов MagicMock в юнит-тестах Python - PullRequest
0 голосов
/ 14 мая 2018

У меня тестируется этот код:

def to_be_tested(x):
  return round((x.a + x.b).c())

В моем unittest я хочу утверждать, что именно это делается с переданным 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() (подробности см. В https://stackoverflow.com/a/50329607/1281485):

unittest.mock._all_magics.add('__round__')
unittest.mock._magics.add('__round__')

Итак, теперь, как я уже сказал, это работает. Но я нахожу это крайне нечитаемым. Кроме того, мне нужно было много поиграть, чтобы найти такие вещи, как _mock_new_parent и т. Д. Подчеркивание также указывает, что это атрибут "private" и его не следует использовать. Документация не упоминает об этом. Здесь также не упоминается другой способ достижения того, чего я пытаюсь достичь.

Есть ли лучший способ проверить возвращенные MagicMock объекты на предмет их создания такими, какими они должны были быть?

1 Ответ

0 голосов
/ 14 мая 2018

Вы идете за борт. Вы тестируете реализацию, а не результат. Более того, вы попадаете во внутреннюю часть фиктивной реализации, к которой вам не нужно прикасаться.

Проверьте, что вы получите правильный результат, и проверьте, что результат основан на входах, которые вы хотите использовать. Вы можете настроить макет так, чтобы 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 уже является доказательством того, что по крайней мере выражение (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, просто сделайте, чтобы ваши mocks создавали эти типы. Например. если ожидается, что какой-то вызов выдаст строку, ваш макет вернет тестовую строку, особенно когда вы затем передаете материал в другие API стандартной библиотеки.
  • издевается над одиночками. Вам не нужно возвращаться из данного макета, чтобы проверить, что у них есть правильный родительский элемент, потому что вы можете добраться до того же объекта, пройдя через родительские атрибуты, а затем использовать is. Например. если функция возвращает где-то фиктивный объект, вы можете утверждать, что фиктивный объект right был возвращен путем тестирования 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'>)]
    
...