我正在使用 pytest 为 Flask 应用程序编写测试。我正在尝试为这段代码编写单元测试。基本上我想模拟
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({}))
这是我的测试:
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("{}")
这是 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
看起来测试失败了,因为在我不希望的地方引发了 FileNotFoundError,但由于它打印了真实文件的内容,我会假设模拟没有生效。
我做错了什么?
只是为了提供一种无需单独课程的替代方案:
@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("{}")
请注意,原始代码中有一些内容不起作用:
patch
和 yield
可以防止这种情况(使用 mocker
是另一种可能性)open
的上下文管理器时,您必须将 __enter__
添加到模拟中(这由上下文管理器调用)None
作为副作用将不起作用,因为在这种情况下 write
将在 None
上调用 - 因此第二个模拟用于 file
我最终只写了自己的课程。这对我有用:
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)
当我需要它时,它会引发 FileNotFoundError 并检查 write 是否被正确调用。可能有更好的方法来做到这一点,但这确实有效!
MrBean Bremen 的才华并非不寻常。
他的解决方案可以稍微简化,具体取决于使用需求。这不使用夹具,只是在测试中执行此操作:
open_mock = mock.MagicMock()
file_mock = mock.Mock()
open_mock.return_value.__enter__.side_effect = [
FileNotFoundError, file_mock
]
with mock.patch('builtins.open', open_mock):
doc.save_as()
我的实验似乎表明,确实,
MagicMock
需要一个open_mock
,但对于Mock
来说,普通的香草file_mock
就可以了。也许您需要 MrBean Bremen 的解决方案中的两种魔法。