4

我定义了这个类:

class InternalProc:

    @staticmethod
    def get_data():
        try:
            result = subprocess.run(['bridge-client', '--business-credentials'],
                                    stdout=subprocess.PIPE)
            data = json.loads(result.stdout.decode('utf-8'))
            return data
        except Exception:
            logger.error("Unable to fetch the data")
            raise

我想对 get_data() 进行单元测试,我应该如何模拟 subprocess.run?我还想断言异常是否被引发?

4

2 回答 2

7

似乎您需要两个测试来测试此方法,一个返回数据,另一个引发Exception.

对于我们返回数据的地方,我们只需要 mock subprocess.run。然后我们可以创建另一个Mock对象来模拟stdout.decode以返回json.loads可以解析的数据。Mock这意味着为 的行为创建一个对象stdout,然后配置我们的模拟subprocess.run对象以使用该对象。

对于另一个测试,我们只需要使用对象的side_effectkwarg 在Mock调用它时引发异常。

给定以下文件夹结构:

stackoverflow/
├── mypackage
│   ├── __init__.py
│   └── proc.py
└── tests
    ├── __init__.py
    └── test_proc.py

我们编写的测试如下所示。

from unittest.mock import MagicMock, patch

import pytest

from mypackage.proc import InternalProc


@patch("mypackage.proc.subprocess.run")
def test_get_data_valid(mock_run):
    mock_stdout = MagicMock()
    mock_stdout.configure_mock(
        **{
            "stdout.decode.return_value": '{"A": 3}'
        }
    )

    mock_run.return_value = mock_stdout

    result = InternalProc.get_data()
    assert result == {"A": 3}


@patch("mypackage.proc.subprocess.run", side_effect=Exception("foobar"))
def test_get_data_invalid(mock_run):
    with pytest.raises(Exception) as exc:
        InternalProc.get_data()
        assert "foobar" in str(exc.value)

======================================= test session starts ========================================
platform darwin -- Python 3.9.1, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: ***/stackoverflow
collected 2 items                                                                                  

tests/test_proc.py ..                                                                        [100%]

======================================== 2 passed in 0.07s =========================================

我给你的建议是,因为我看到你在过去几天发布了多个pytest/问题,所以花一些时间阅读两者的文档。mock制作一些玩具示例并玩弄这两个包。这是您学习如何模拟以及在何处模拟的唯一方法。

于 2021-02-23T12:47:51.193 回答
0

我将它用于一个测试套件,其中有许多 subprocess.run 调用需要检查。

class CmdMatch(NamedTuple):
    cmd: str
    match: str = ".*"
    result: str = ""
    side_effect: Callable = None


@contextlib.contextmanager
def mock_run(*cmd_match: Union[str, CmdMatch], **kws):
    sub_run = subprocess.run
    mock = MagicMock()
    if isinstance(cmd_match[0], str):
        cmd_match = [CmdMatch(*cmd_match, **kws)]

    def new_run(cmd, **_kws):
        check_cmd = " ".join(cmd[1:])
        mock(*cmd[1:])
        for m in cmd_match:
            if m.cmd in cmd[0].lower() and re.match(m.match, check_cmd):
                if m.side_effect:
                    m.side_effect()
                return subprocess.CompletedProcess(cmd, 0, m.result, "")
        assert False, "No matching call for %s" % check_cmd

    subprocess.run = new_run
    yield mock
    subprocess.run = sub_run

所以现在你可以写一个这样的测试:


def test_call_git():
    with mock_run("git", "describe.*", "v2.5") as cmd:
       do_something()
       cmd.assert_called_once()

def test_other_calls():
    with mock_run(CmdMatch("git", "describe.*", "v2.5"), CmdMatch("aws", "s3.*links.*", side_effect=CalledProcessError) as cmd:
       with pytest.raises(CalledProcessError):
           do_something()

几个你可能想改变的时髦的东西,但我喜欢它们:

  • 生成的模拟调用忽略第一个参数(因此 which/full-paths 不是测试的一部分)
  • 如果没有匹配任何调用,它断言(你必须有一个匹配)
  • 第一个位置被认为是“命令的名称”
  • 要运行的 kwargs 被忽略(_call 断言很容易但很松散)
于 2022-01-14T21:59:52.957 回答