如何在使用 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 并且再次模拟将起作用。