如何在使用 pytest-console-scripts 测试的 python 脚本中模拟函数?
How to mock a function within a python script tested with pytest-console-scripts?
我需要测试一些遗留代码,其中有一些Python脚本。
通过脚本我的意思是 Python 代码不在 class 或模块中,只是在一个独特的文件中并使用 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
来测试它,因为它的“进程中”启动器可以实际模拟一些东西。
AFAIU,当 运行 和 subprocess
时,无法模拟在 Python 脚本中进行的任何调用
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()
可以mock成功,但是unmockable
函数可以mock吗?那是我的问题。
是否有另一种策略来测试此类 Python 脚本并能够模拟内部函数?
这里的问题是当脚本执行时,oldscript.py
没有被导入到 oldscript
命名空间,而是在 __main__
中(这就是为什么 [=脚本底部的 16=] 为真)。您的代码已成功修补 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
,一切都会按预期进行。
另一种方法是在 setup.py
中使用 console_scripts
入口点(有关详细信息,请参阅 here)。这是一种更复杂的方法,非常适合 python 具有 setup.py
并已安装(例如通过 pip
)的软件包。
当您设置要以这种方式安装和调用的脚本时,它会在 PATH
中可用,例如oldscript
然后你可以从测试中调用它:
script_runner.run('oldscript') # Without the .py
这些已安装的控制台脚本是使用 setup.py
创建的包装器导入和执行的,因此 oldscript.py
将被导入为 oldscript
并且再次模拟将起作用。
我需要测试一些遗留代码,其中有一些Python脚本。
通过脚本我的意思是 Python 代码不在 class 或模块中,只是在一个独特的文件中并使用 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
来测试它,因为它的“进程中”启动器可以实际模拟一些东西。
AFAIU,当 运行 和 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()
可以mock成功,但是unmockable
函数可以mock吗?那是我的问题。
是否有另一种策略来测试此类 Python 脚本并能够模拟内部函数?
这里的问题是当脚本执行时,oldscript.py
没有被导入到 oldscript
命名空间,而是在 __main__
中(这就是为什么 [=脚本底部的 16=] 为真)。您的代码已成功修补 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
,一切都会按预期进行。
另一种方法是在 setup.py
中使用 console_scripts
入口点(有关详细信息,请参阅 here)。这是一种更复杂的方法,非常适合 python 具有 setup.py
并已安装(例如通过 pip
)的软件包。
当您设置要以这种方式安装和调用的脚本时,它会在 PATH
中可用,例如oldscript
然后你可以从测试中调用它:
script_runner.run('oldscript') # Without the .py
这些已安装的控制台脚本是使用 setup.py
创建的包装器导入和执行的,因此 oldscript.py
将被导入为 oldscript
并且再次模拟将起作用。