首页 > 解决方案 > 如何在 Python 上设置模拟异常行为?

问题描述

我正在使用定义内部异常(github3.exceptions.UnprocessableEntity)的外部库(github3.py)。这个异常是如何定义的并不重要,所以我想创建一个副作用并设置我从这个异常中使用的属性。

测试代码不是那么简单的例子:

import github3

class GithubService:
    def __init__(self, token: str) -> None:
        self.connection = github3.login(token=token)
        self.repos = self.connection.repositories()

    def create_pull(self, repo_name: str) -> str:
        for repo in self.repos:
            if repo.full_name == repo_name:
                break
        try:
            created_pr = repo.create_pull(
                title="title",
                body="body",
                head="head",
                base="base",
            )
        except github3.exceptions.UnprocessableEntity as github_exception:
            extra = ""
            for error in github_exception.errors:
                if "message" in error:
                    extra += f"{error['message']} "
                else:
                    extra += f"Invalid field {error['field']}. " # testing this case
            return f"{repo_name}: {github_exception.msg}. {extra}"

我需要设置属性msg以及errors异常。所以我尝试使用 pytest-mock 在我的测试代码中:

@pytest.fixture
def mock_github3_login(mocker: MockerFixture) -> MockerFixture:
    """Fixture for mocking github3.login."""
    mock = mocker.patch("github3.login", autospec=True)
    mock.return_value.repositories.return_value = [
        mocker.Mock(full_name="staticdev/nope"),
        mocker.Mock(full_name="staticdev/omg"),
    ]
    return mock


def test_create_pull_invalid_field(
    mocker: MockerFixture, mock_github3_login: MockerFixture,
) -> None:
    exception_mock = mocker.Mock(errors=[{"field": "head"}], msg="Validation Failed")
    mock_github3_login.return_value.repositories.return_value[1].create_pull.side_effect = github3.exceptions.UnprocessableEntity(mocker.Mock())
    mock_github3_login.return_value.repositories.return_value[1].create_pull.return_value = exception_mock
    response = GithubService("faketoken").create_pull("staticdev/omg")

    assert response == "staticdev/omg: Validation Failed. Invalid field head."

这段代码的问题是,如果你有side_effectand return_valuePython 就会忽略 return_value

这里的问题是我不想知道UnprocessableEntity调用它的实现,将正确的参数传递给它的构造函数。另外,我没有找到使用 just 的其他方法side_effect。我还尝试使用返回值并设置模拟并以这种方式使用它:

def test_create_pull_invalid_field(
    mock_github3_login: MockerFixture,
) -> None:
    exception_mock = Mock(__class__ = github3.exceptions.UnprocessableEntity, errors=[{"field": "head"}], msg="Validation Failed")
    mock_github3_login.return_value.repositories.return_value[1].create_pull.return_value = exception_mock
    response = GithubService("faketoken").create_pull("staticdev/omg")

    assert response == "staticdev/omg: Validation Failed. Invalid field head."

这也不起作用,不会抛出异常。所以我不知道如何克服这个问题,因为我不想看到UnprocessableEntity. 这里有什么想法吗?

标签: python-3.xunit-testingexceptionpytestpytest-mock

解决方案


因此,根据您的示例,您实际上并不需要模拟github3.exceptions.UnprocessableEntity而只需要模拟传入的resp参数。

所以下面的测试应该有效:

def test_create_pull_invalid_field(
    mocker: MockerFixture, mock_github3_login: MockerFixture,
) -> None:
    mocked_response = mocker.Mock()
    mocked_response.json.return_value = {
        "message": "Validation Failed", "errors": [{"field": "head"}]
    }

    repo = mock_github3_login.return_value.repositories.return_value[1]
    repo.create_pull.side_effect = github3.exceptions.UnprocessableEntity(mocked_response)
    response = GithubService("faketoken").create_pull("staticdev/omg")

    assert response == "staticdev/omg: Validation Failed. Invalid field head."

编辑:

如果您希望github3.exceptions.UnprocessableEntity完全抽象,则无法模拟整个类,因为不允许捕获不继承自 BaseException 的类(请参阅docs)。但是您可以通过仅模拟构造函数来解决它:

def test_create_pull_invalid_field(
    mocker: MockerFixture, mock_github3_login: MockerFixture,
) -> None:
    def _initiate_mocked_exception(self) -> None:
        self.errors = [{"field": "head"}]
        self.msg = "Validation Failed"

    mocker.patch.object(
        github3.exceptions.UnprocessableEntity, "__init__", 
        _initiate_mocked_exception
    )

    repo = mock_github3_login.return_value.repositories.return_value[1]
    repo.create_pull.side_effect = github3.exceptions.UnprocessableEntity
    response = GithubService("faketoken").create_pull("staticdev/omg")

    assert response == "staticdev/omg: Validation Failed. Invalid field head."


推荐阅读