Функция тестирования с изменяемым аргументом

#python #pytest

#python #pytest

Вопрос:

Я пишу несколько тестов для функции, которая передает экземпляр коллекций.Счетчик другой функции, которую мне нужно издеваться. Вот небольшой пример, представляющий мой код:

 # 'bar' module

def callback(counter):
    """Do something with the *counter*."""
    pass


def update():
    """Update counter and pass it to callback."""
    counter = collections.Counter()

    for _ in range(3):
        counter.update(["foo"])
        callback(counter)
  

Следующий тест не проходит, поскольку тот же экземпляр счетчика был изменен и передан функции ‘обратного вызова’. Следовательно, утверждение будет передаваться только тогда, когда все вызовы принимают последнее значение счетчика, которое collections.Counter({"foo": 3})

 @pytest.fixture()
def mocked_callback(mocker):
    return mocker.patch("bar.callback")


def test_update(mocker, mocked_callback):
    update()

    assert mocked_callback.call_args_list == [
        mocker.call(collections.Counter({"foo": 1})),
        mocker.call(collections.Counter({"foo": 2})),
        mocker.call(collections.Counter({"foo": 3})),
    ]
  

Кто-нибудь знает хороший способ проанализировать точное состояние измененного объекта, когда он передается в издевательскую функцию?

Комментарии:

1. Вы уверены, что издеваетесь над правильной функцией? Значение __name__ int в тестовом примере, вероятно, может отличаться от имени модуля, который вы пытаетесь протестировать…

2. Здесь в коде есть пара ошибок (упомянутая __name__ и путаница callback и do_something ), но основная проблема заключается в том, что вызовы ссылаются на dict, который обновляется во время вызовов, так что после вызовов все вызовы указывают на один и тот же (окончательный) dict {"foo": 3} (not {"foo": 1} ) . Я не уверен, как это решить…

3. Спасибо, я исправил опечатку, которую вы заметили. Также мои реальные тесты не смешиваются в том же модуле, что и тестируемый код, я просто использовал __name__ для предоставления примера для копирования 🙂

4. @MrBeanBremen Это именно та проблема, которую я пытаюсь решить, да. Прямо сейчас мой единственный обходной путь — шпионить collections.Counter.update вместо проверки аргументов обратного вызова, но я чувствую, что должен быть лучший способ..

Ответ №1:

Эта проблема фактически упоминается в документации unittest.

Другая ситуация встречается редко, но может вас укусить, когда ваш макет вызывается с изменяемыми аргументами. call_args и call_args_list хранят ссылки на аргументы. Если аргументы изменены тестируемым кодом, вы больше не можете делать утверждения о том, какие значения были при вызове макета.

Предложенный обходной путь, похоже, работает отлично!

 @pytest.fixture()
def mocked_callback(mocker):

    class _CopyingMock(mocker.MagicMock):
        """Extended Mocker to copy arguments on each call."""

        def __call__(self, *args, **kwargs):
            args = copy.deepcopy(args)
            kwargs = copy.deepcopy(kwargs)
            return super(_MagicMock, self).__call__(*args, **kwargs)

    return mocker.patch("bar.callback", _CopyingMock())
  

Есть заявки на включение этой функции в основную библиотеку:

Комментарии:

1. Приятно! На самом деле я думал об этом как об обходном пути (только что написал соответствующий комментарий), но я не знал, что это есть в документации.

2. На самом деле я собирался спросить о репозитории pytest-mock, когда я наткнулся на аналогичный вопрос , вот где я нашел ссылку 🙂