如何根据传递给 open() 的参数以不同方式模拟打开
How to mock open differently depending on the parameters passed to open()
我的问题是如何在 python 中模拟打开,使其根据调用 open() 的参数做出不同的反应。这些是一些应该可能的不同场景:
- 打开模拟文件;阅读预设内容,基本场景
- 打开两个模拟文件并让它们为 read() 方法返回不同的值。文件 opened/read 的顺序不应影响结果。
- 此外,如果我调用
open('actual_file.txt')
打开一个实际文件,我希望打开实际文件,而不是具有模拟行为的魔术模拟。或者,如果我只是不想模拟访问某个文件,但我确实希望模拟其他文件,这应该是可能的。
我知道这个问题:Python mock builtin 'open' in a class using two different files。
但是这个答案只能部分回答第二个要求。关于顺序独立结果的部分不包括在内,它没有指定如何只模拟一些调用,并允许其他调用通过实际文件(默认行为)。
这可以通过遵循其他问题的已接受答案 (Python mock builtin 'open' in a class using two different files) 中的方法并进行一些改动来完成。
首先。而不是仅仅指定一个可以弹出的 side_effect
。我们需要确保 side_effect
可以 return 正确 mocked_file 取决于 open
调用使用的参数。
然后,如果我们希望打开的文件不在我们希望模拟的文件中,我们将 return 文件的原始 open()
而不是任何模拟行为。
下面的代码演示了如何以干净、可重复的方式实现这一点。例如,我将这段代码放在一个文件中,该文件提供了一些实用功能以使测试更容易。
from mock import MagicMock
import __builtin__
from mock import patch
import sys
# Reference to the original open function.
g__test_utils__original_open = open
g__test_utils__file_spec = None
def create_file_mock(read_data):
# Create file_spec such as in mock.mock_open
global g__test_utils__file_spec
if g__test_utils__file_spec is None:
# set on first use
if sys.version_info[0] == 3:
import _io
g__test_utils__file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO))))
else:
g__test_utils__file_spec = file
file_handle = MagicMock(spec=g__test_utils__file_spec)
file_handle.write.return_value = None
file_handle.__enter__.return_value = file_handle
file_handle.read.return_value = read_data
return file_handle
def flexible_mock_open(file_map):
def flexible_side_effect(file_name):
if file_name in file_map:
return file_map[file_name]
else:
global g__test_utils__original_open
return g__test_utils__original_open(file_name)
global g__test_utils__original_open
return_value = MagicMock(name='open', spec=g__test_utils__original_open)
return_value.side_effect = flexible_side_effect
return return_value
if __name__ == "__main__":
a_mock = create_file_mock(read_data="a mock - content")
b_mock = create_file_mock(read_data="b mock - different content")
mocked_files = {
'a' : a_mock,
'b' : b_mock,
}
with patch.object(__builtin__, 'open', flexible_mock_open(mocked_files)):
with open('a') as file_handle:
print file_handle.read() # prints a mock - content
with open('b') as file_handle:
print file_handle.read() # prints b mock - different content
with open('actual_file.txt') as file_handle:
print file_handle.read() # prints actual file contents
这直接从 mock.py (python 2.7) 中借用了一些代码来创建 file_spec。
旁注:如果有任何机构可以帮助我尽可能隐藏这些全局变量,那将非常有帮助。
有点晚了,但我最近刚好遇到同样的需求,所以我想分享我的解决方案,基于 this answer from the referred-to question:
import pytest
from unittest.mock import mock_open
from functools import partial
from pathlib import Path
mock_file_data = {
"file1.txt": "some text 1",
"file2.txt": "some text 2",
# ... and so on ...
}
do_not_mock: {
# If you need exact match (see note in mocked_file(),
# you should replace these with the correct Path() invocations
"notmocked1.txt",
"notmocked2.txt",
# ... and so on ...
}
# Ref:
def mocked_file(m, fn, *args, **kwargs):
m.opened_file = Path(fn)
fn = Path(fn).name # If you need exact path match, remove this line
if fn in do_not_mock:
return open(fn, *args, **kwargs)
if fn not in mock_file_data:
raise FileNotFoundError
data = mock_file_data[fn]
file_obj = mock_open(read_data=data).return_value
file_obj.__iter__.return_value = data.splitlines(True)
return file_obj
def assert_opened(m, fn):
fn = Path(fn)
assert m.opened_file == fn
@pytest.fixture()
def mocked_open(mocker):
m = mocker.patch("builtins.open")
m.side_effect = partial(mocked_file, m)
m.assert_opened = partial(assert_opened, m)
return m
def test_something(mocked_open):
...
# Something that should NOT invoke open()
mocked_open.assert_not_called()
...
# Something that SHOULD invoke open()
mocked_open.assert_called_once()
mocked_open.assert_opened("file1.txt")
# Depends on how the tested unit handle "naked" filenames,
# you might have to change the arg to:
# Path.cwd() / "file1.txt"
# ... and so on ...
请注意 (1) 我正在使用 Python 3,并且 (2) 我正在使用 pytest
.
我的问题是如何在 python 中模拟打开,使其根据调用 open() 的参数做出不同的反应。这些是一些应该可能的不同场景:
- 打开模拟文件;阅读预设内容,基本场景
- 打开两个模拟文件并让它们为 read() 方法返回不同的值。文件 opened/read 的顺序不应影响结果。
- 此外,如果我调用
open('actual_file.txt')
打开一个实际文件,我希望打开实际文件,而不是具有模拟行为的魔术模拟。或者,如果我只是不想模拟访问某个文件,但我确实希望模拟其他文件,这应该是可能的。
我知道这个问题:Python mock builtin 'open' in a class using two different files。 但是这个答案只能部分回答第二个要求。关于顺序独立结果的部分不包括在内,它没有指定如何只模拟一些调用,并允许其他调用通过实际文件(默认行为)。
这可以通过遵循其他问题的已接受答案 (Python mock builtin 'open' in a class using two different files) 中的方法并进行一些改动来完成。
首先。而不是仅仅指定一个可以弹出的 side_effect
。我们需要确保 side_effect
可以 return 正确 mocked_file 取决于 open
调用使用的参数。
然后,如果我们希望打开的文件不在我们希望模拟的文件中,我们将 return 文件的原始 open()
而不是任何模拟行为。
下面的代码演示了如何以干净、可重复的方式实现这一点。例如,我将这段代码放在一个文件中,该文件提供了一些实用功能以使测试更容易。
from mock import MagicMock
import __builtin__
from mock import patch
import sys
# Reference to the original open function.
g__test_utils__original_open = open
g__test_utils__file_spec = None
def create_file_mock(read_data):
# Create file_spec such as in mock.mock_open
global g__test_utils__file_spec
if g__test_utils__file_spec is None:
# set on first use
if sys.version_info[0] == 3:
import _io
g__test_utils__file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO))))
else:
g__test_utils__file_spec = file
file_handle = MagicMock(spec=g__test_utils__file_spec)
file_handle.write.return_value = None
file_handle.__enter__.return_value = file_handle
file_handle.read.return_value = read_data
return file_handle
def flexible_mock_open(file_map):
def flexible_side_effect(file_name):
if file_name in file_map:
return file_map[file_name]
else:
global g__test_utils__original_open
return g__test_utils__original_open(file_name)
global g__test_utils__original_open
return_value = MagicMock(name='open', spec=g__test_utils__original_open)
return_value.side_effect = flexible_side_effect
return return_value
if __name__ == "__main__":
a_mock = create_file_mock(read_data="a mock - content")
b_mock = create_file_mock(read_data="b mock - different content")
mocked_files = {
'a' : a_mock,
'b' : b_mock,
}
with patch.object(__builtin__, 'open', flexible_mock_open(mocked_files)):
with open('a') as file_handle:
print file_handle.read() # prints a mock - content
with open('b') as file_handle:
print file_handle.read() # prints b mock - different content
with open('actual_file.txt') as file_handle:
print file_handle.read() # prints actual file contents
这直接从 mock.py (python 2.7) 中借用了一些代码来创建 file_spec。
旁注:如果有任何机构可以帮助我尽可能隐藏这些全局变量,那将非常有帮助。
有点晚了,但我最近刚好遇到同样的需求,所以我想分享我的解决方案,基于 this answer from the referred-to question:
import pytest
from unittest.mock import mock_open
from functools import partial
from pathlib import Path
mock_file_data = {
"file1.txt": "some text 1",
"file2.txt": "some text 2",
# ... and so on ...
}
do_not_mock: {
# If you need exact match (see note in mocked_file(),
# you should replace these with the correct Path() invocations
"notmocked1.txt",
"notmocked2.txt",
# ... and so on ...
}
# Ref:
def mocked_file(m, fn, *args, **kwargs):
m.opened_file = Path(fn)
fn = Path(fn).name # If you need exact path match, remove this line
if fn in do_not_mock:
return open(fn, *args, **kwargs)
if fn not in mock_file_data:
raise FileNotFoundError
data = mock_file_data[fn]
file_obj = mock_open(read_data=data).return_value
file_obj.__iter__.return_value = data.splitlines(True)
return file_obj
def assert_opened(m, fn):
fn = Path(fn)
assert m.opened_file == fn
@pytest.fixture()
def mocked_open(mocker):
m = mocker.patch("builtins.open")
m.side_effect = partial(mocked_file, m)
m.assert_opened = partial(assert_opened, m)
return m
def test_something(mocked_open):
...
# Something that should NOT invoke open()
mocked_open.assert_not_called()
...
# Something that SHOULD invoke open()
mocked_open.assert_called_once()
mocked_open.assert_opened("file1.txt")
# Depends on how the tested unit handle "naked" filenames,
# you might have to change the arg to:
# Path.cwd() / "file1.txt"
# ... and so on ...
请注意 (1) 我正在使用 Python 3,并且 (2) 我正在使用 pytest
.