#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-консоли.