Как имитировать функцию в скрипте python, протестированном с помощью сценариев pytest-консоли?

#python #mocking #pytest

Вопрос:

Мне нужно протестировать некоторый устаревший код, среди которого есть несколько сценариев на Python. Под сценарием я подразумеваю код Python не внутри класса или модуля, а просто в уникальном файле и выполняемый с python script.py

Вот пример oldscript.py :

 import socket


def testfunction():
    return socket.gethostname()


def unmockable():
    return "somedata"


if __name__ == '__main__':
    result = testfunction()
    result = unmockable()
    print(result)
 

Я использую pytest-console-scripts это для тестирования, так как пусковая установка «inprocess» позволяет на самом деле издеваться над некоторыми вещами.

AFAIU, нет никакого способа издеваться над любым вызовом, выполняемым в скрипте Python, когда он выполняется с subprocess

pytest-console-scripts делает это возможным и действительно издевается над работой внешних функций.

Вот тестовый пример для вышеперечисленного :

 import socket
from pytest_console_scripts import ScriptRunner
from pytest_mock import MockerFixture


class TestOldScript:

    def test_success(self, script_runner: ScriptRunner, mocker: MockerFixture) -> None:
        mocker.patch('socket.gethostname', return_value="myhostname")
        mocker.patch('oldscript.unmockable', return_value="mocked!", autospec=True)
        ret = script_runner.run('oldscript.py', print_result=True, shell=True)
        socket.gethostname.assert_called_with()
        assert ret.success
        assert ret.stdout == 'mocked!'
        assert ret.stderr is None
 

Это неудача, так как unmockable нельзя так издеваться.

Вызов socket.gethostname() может быть успешно издевательски обработан, но можно unmockable ли издеваться над функцией? Это моя проблема.

Будет ли существовать другая стратегия для тестирования таких сценариев Python и возможности имитировать внутренние функции?

Ответ №1:

Проблема здесь в том, что при выполнении сценария oldscript.py он не импортируется в oldscript пространство имен, а вместо этого находится в __main__ (вот почему условие if в нижней части сценария верно). Ваш код успешно исправлен oldscript.unmockable , но скрипт вызывает __main__.unmockable , и его действительно невозможно заблокировать.

Я вижу два способа обойти это:

Вы можете разделить код, который вы хотели бы воспроизвести, на другой модуль, импортируемый основным сценарием. Например, если вы разделитесь oldscript.py на два файла, как это:

lib.py :

 def unmockable():
    return "somedata"
 

oldscript.py :

 import socket
import lib


def testfunction():
    return socket.gethostname()


if __name__ == '__main__':
    result = testfunction()
    print('testfunction:', result)
    result = lib.unmockable()
    print('unmockable:', result)
 

затем вы можете поиздеваться lib.unmockable в тесте, и все будет работать так, как ожидалось.

Другой подход заключается в использовании точки console_scripts входа setup.py (подробнее об этом см. Здесь). Это более сложный подход, который хорошо подходит для пакетов python, которые установлены setup.py и установлены (например, через pip ).

Когда вы настраиваете свой скрипт для установки и вызова таким образом, он становится доступным в PATH качестве, например oldscript , а затем вы можете вызвать его из тестов с помощью:

 script_runner.run('oldscript')  # Without the .py
 

Эти установленные консольные сценарии импортируются и выполняются с помощью оболочки, которая setup.py создает, поэтому oldscript.py они будут импортированы, oldscript и снова будет работать насмешка.

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

1. Поучительно! Я защищаю. необходимо лучше понять пространства имен Python. Как правило, мне нравится иметь возможность протестировать сценарии, прежде чем я начну их изменять. Поэтому мне больше нравится второй подход, даже если для этого потребуется создать setup.py . Хотя рефакторинг нескольких функций во внешнюю библиотеку/помощник должен быть безопасным, это создает вероятность ошибки, которая была бы крайне нежелательна при разработке…

2. Я согласен. При добавлении тестов в сценарии, в которых раньше не было тестов, невозможность издеваться перед перемещением может быть существенным ограничением. Вероятно, можно издеваться над вещами внутри __main__ пространства имен после его создания, но это потребует некоторых специальных хакерских действий. Может быть, когда-нибудь я придумаю, как добавить это в сценарии pytest-консоли.