简体   繁体   English

使用 mock_open 引发 FileNotFoundError 并检查是否调用了 write

[英]Using mock_open to raise a FileNotFoundError and check if write is called

I'm working on writing tests for a flask application using pytest.我正在使用 pytest 为烧瓶应用程序编写测试。 I'm trying to write a unit test for this code.我正在尝试为此代码编写单元测试。 Basically I want to mock open() so that it throws a FileNotFoundError when I call open the first time and check that write() is called once.基本上我想模拟open()以便在我第一次调用 open 时抛出 FileNotFoundError 并检查write()是否被调用一次。

try:
    with open("config.json", "r") as file:
        config = json.loads(file.read())
        print(config)
except FileNotFoundError:
    with open("config.json", "w") as file:
        file.write(json.dumps({}))

This is my test:这是我的测试:

import pytest
import mock
@pytest.fixture
def client(mocker):
    return app.test_client()
@pytest.fixture
def mock_no_config(mocker):
    m = mock.mock_open()
    m.side_effect = [FileNotFoundError, None]
    mock.patch("builtins.open", m)
    return m

def test_no_config(client, mock_database_exists, mock_no_config):
    response = client.get("/install/")
    mock_no_config().write.assert_called_once_with("{}")

This is the output from pytest这是 pytest 的输出

====================================================================== FAILURES =======================================================================
___________________________________________________________________ test_no_config ____________________________________________________________________

client = <FlaskClient <Flask 'main'>>, mock_database_exists = None
mock_no_config = <MagicMock name='open' spec='builtin_function_or_method' id='139985038428816'>

    def test_no_config(client, mock_database_exists, mock_no_config):
        response = client.get("/install/")
>       mock_no_config().write.assert_called_once_with("{}")

tests/test_web_installer.py:44:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
venv/lib/python3.10/site-packages/mock/mock.py:1100: in __call__
    return _mock_self._mock_call(*args, **kwargs)
venv/lib/python3.10/site-packages/mock/mock.py:1104: in _mock_call
    return _mock_self._execute_mock_call(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

_mock_self = <MagicMock name='open' spec='builtin_function_or_method' id='139985038428816'>, args = (), kwargs = {}
self = <MagicMock name='open' spec='builtin_function_or_method' id='139985038428816'>, effect = <list_iterator object at 0x7f50ce47f880>
result = <class 'FileNotFoundError'>

    def _execute_mock_call(_mock_self, *args, **kwargs):
        self = _mock_self
        # separate from _increment_mock_call so that awaited functions are
        # executed separately from their call, also AsyncMock overrides this method

        effect = self.side_effect
        if effect is not None:
            if _is_exception(effect):
                raise effect
            elif not _callable(effect):
                result = next(effect)
                if _is_exception(result):
>                   raise result
E                   FileNotFoundError

venv/lib/python3.10/site-packages/mock/mock.py:1165: FileNotFoundError
---------------------------------------------------------------- Captured stdout call -----------------------------------------------------------------
<THE CONTENTS OF THE REAL config.json FILE HERE>
=============================================================== short test summary info ===============================================================
FAILED tests/test_web_installer.py::test_no_config - FileNotFoundError

It looks like the test is failing because a FileNotFoundError is being raised somewhere I'm not expecting it to, but since it's printing the contents of the real file, I would've assumed the mock wasn't taking effect.看起来测试失败了,因为 FileNotFoundError 正在某个我没想到的地方引发,但由于它正在打印真实文件的内容,我会假设模拟没有生效。

What am I doing wrong?我究竟做错了什么?

I ended up just writing my own class.我最终只写了我自己的课。 This did the trick for me:这对我有用:

class FakeOpen():
    def __init__(self, filename, mode):
        if filename == "config.json" and mode == "r":
            raise FileNotFoundError()
        self.filename = filename
        self.mode = mode
        self.write_calls = 0

    def read(self):
        return b"{}"

    def write(self, write_string):
        self.write_calls += 1
        assert self.write_calls <= 1
        assert write_string == "{}"

    def close(self):
        if self.mode == "w":
            assert self.write_calls == 1

    def __enter__(self):
        return self

    def __exit__(self, *args, **kwargs):
        pass
mocker.patch("builtins.open", FakeOpen)

It raises the FileNotFoundError when I need it to and checks that write is called properly.当我需要它时它会引发 FileNotFoundError 并检查 write 是否被正确调用。 There's probably a better way to do this, but this does the trick!可能有更好的方法来做到这一点,但这可以解决问题!

Just to give an alternative without a separate class:只是为了提供一个没有单独课程的替代方案:

@pytest.fixture
def mock_no_config():
    open_mock = MagicMock()  # mock for open
    file_mock = MagicMock()  # mock for file returned from open
    open_mock.return_value.__enter__.side_effect = [
        FileNotFoundError, file_mock
    ]
    with mock.patch("builtins.open", open_mock):
        yield file_mock


def test_no_config(client, mock_database_exists, mock_no_config):
    response = client.get("/install/")
    mock_no_config.write.assert_called_once_with("{}")

Note that there are a few things in the original code that won't work:请注意,原始代码中有一些内容不起作用:

  • the mocking was reversed as soon as the fixture was returned - using the context manager version of patch together with yield prevents this (using mocker is another possibility)一旦夹具返回,模拟就被逆转了——使用patch的上下文管理器版本和yield可以防止这种情况(使用mocker是另一种可能性)
  • as you use a context manager with open , you have to add __enter__ to the mock (this is called by the context manager)当您使用带有open的上下文管理器时,您必须将__enter__添加到模拟中(这由上下文管理器调用)
  • using None as side effect won't work, as in this case write would be called on None - thus the second mock used for file使用None作为副作用将不起作用,因为在这种情况下write将在None上调用 - 因此第二个模拟用于file

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM