Python pytest mock pathlib.Path.open

Python pytest mock pathlib.Path.open,python,python-3.x,unit-testing,pytest,pytest-mock,Python,Python 3.x,Unit Testing,Pytest,Pytest Mock,我需要模拟pathlib.Path。使用pytest mock打开 realopen_func打开一个yaml文件。返回值是一个常规的dict。如何模拟Path.open加载另一个名为test config.yaml的yaml文件 我的代码工作不正常,因为conf将变成str(“test_config.yaml”)。它应该是一个dict from pathlib import Path import yaml def open_func(): with Path.open(Path

我需要模拟
pathlib.Path。使用
pytest mock打开

real
open_func
打开一个
yaml文件
。返回值是一个常规的
dict
。如何模拟
Path.open
加载另一个名为
test config.yaml
yaml文件

我的代码工作不正常,因为
conf
将变成
str
(“test_config.yaml”)。它应该是一个
dict

from pathlib import Path

import yaml


def open_func():
    with Path.open(Path("./config.yaml")) as f:
        return yaml.load(f, Loader=yaml.FullLoader)


def test_open_func(mocker):
    mocker.patch("pathlib.Path.open", mocker.mock_open(read_data="test_config.yaml"))
    conf = open_func()

    assert isinstance(conf, dict)
编辑: 为了更接近我的实际问题,我提供了以下代码。我有一个类
TryToMock
,它基本上接受两个文件作为输入。方法
load_files
只是加载这些文件(实际上是.yaml文件)并返回输出。这些.yaml文件实际上是一些配置文件

在我的单元测试中,我将通过pytest的
参数化
多次调用
TryToMock
。因此,我希望通过
夹具
加载原始配置文件。然后,在运行
加载\u文件之前,我可以
monkeypatch
各种测试中的一些条目

为了不再次加载原始文件,我需要模拟
路径。在
TryToMock
中打开
函数。我想传递
monkeypatched
yaml文件(即以
dict
的形式)。困难在于我必须区分这两个文件。也就是说,我不能简单地用相同的文件内容模拟
Path.open
函数

# TryToMock.py

from pathlib import Path
import yaml

# In my current working folder, I have to .yaml files containing the following
# content for illustrative purpose:
#
# file1.yaml = {'name': 'test1', 'file_type': 'yaml'}
# file2.yaml = {'schema': 'test2', 'currencies': ['EUR', 'USD', 'JPY']}


class TryToMock:
    def __init__(self, file_to_mock_1, file_to_mock_2):
        self._file_to_mock_1 = file_to_mock_1
        self._file_to_mock_2 = file_to_mock_2

    def load_files(self):
        with Path.open(self._file_to_mock_1) as f:
            file1 = yaml.load(f, Loader=yaml.FullLoader)

        with Path.open(self._file_to_mock_2) as f:
            file2 = yaml.load(f, Loader=yaml.FullLoader)

        return file1, file2




# test_TryToMock.py

import os
from pathlib import Path

import pytest
import yaml

from tests import TryToMock


def yaml_files_for_test(yaml_content):
    names = {"file1.yaml": file1_content, "file2.yaml": file2_content}
    return os.path.join("./", names[os.path.basename(yaml_content)])


@pytest.fixture(scope="module")
def file1_content():
    with Path.open(Path("./file1.yaml")) as f:
        return yaml.load(f, Loader=yaml.FullLoader)


@pytest.fixture(scope="module")
def file2_content():
    with Path.open(Path("./file2.yaml")) as f:
        return yaml.load(f, Loader=yaml.FullLoader)


def test_try_to_mock(file1_content, file2_content, monkeypatch, mocker):
    file_1 = Path("./file1.yaml")
    file_2 = Path("./file2.yaml")

    m = TryToMock.TryToMock(file_to_mock_1=file_1, file_to_mock_2=file_2)

    # Change some items
    monkeypatch.setitem(file1_content, "file_type", "json")

    # Mocking - How does it work when I would like to use mock_open???
    # How should the lambda function look like?
    mocker.patch(
        "pathlib.Path.open",
        lambda x: mocker.mock_open(read_data=yaml_files_for_test(x)),
    )

    files = m.load_files()
    assert files[0]["file_type"] == "json"

您必须向
mock\u open
read\u data
参数提供实际的文件内容。您只需在测试中创建数据:

test_yaml = """
foo:
  bar:
    - VAR: "MyVar"
"""

def test_open_func(mocker):
    mocker.patch("pathlib.Path.open", mocker.mock_open(read_data=test_yaml))
    conf = open_func()
    assert conf == {'foo': {'bar': [{'VAR': 'MyVar'}]}}
或者,您可以从测试文件中读取数据:

def test_open_func(mocker):
    with open("my_fixture_path/test.yaml") as f:
        contents = f.read()
    mocker.patch("pathlib.Path.open", mocker.mock_open(read_data=contents))
    conf = open_func()
    assert isinstance(conf, dict)
最后一个案例也可以重新写入,用测试路径替换
open
调用中的
path
参数:

def test_open_func(mocker):
    mocker.patch("pathlib.Path.open", lambda path: open("test.yaml"))
    conf = open_func()
    assert isinstance(conf, dict)
或者,如果您有不同配置的不同测试文件,则类似于:

def yaml_path_for_test(yaml_path):
    names = {
        "config.yaml": "test.yaml",
        ...
    }
    return os.path.join(my_fixture_path, names[os.path.basename(yaml_path)])

def test_open_func3(mocker):
    mocker.patch("pathlib.Path.open", lambda path: open(yaml_path_for_test(path)))
    conf = open_func()
    assert isinstance(conf, dict)
这可能是您希望在测试代码中实现的

更新: 这与问题的第二部分有关(编辑后)。如果您有模块范围的装置,如问题中所述预加载装置文件,则可以执行以下操作:

def test_open_func(mocker, file1_content, file2_content):
    def yaml_files_for_test(path):
        contents = {"file1.yaml": file1_content,
                    "file2.yaml": file2_content}
        data = contents[os.path.basename(path)]
        mock = mocker.mock_open(read_data=yaml.dump(data))
        return mock.return_value

    mocker.patch("pathlib.Path.open", yaml_files_for_test)
    conf = open_func()
    assert isinstance(conf, dict)
或者,如果您不喜欢使用嵌套函数:

def yaml_files_for_test(path, mocker, content1, content2):
    contents = {"file1.yaml": content1,
                "file2.yaml": content2}
    data = contents[os.path.basename(path)]
    mock = mocker.mock_open(read_data=yaml.dump(data))
    return mock.return_value


def test_open_func5(mocker, file1_content, file2_content):
    mocker.patch("pathlib.Path.open",
                 lambda path: yaml_files_for_test(path, mocker,
                                                  file2_content, file2_content))
    conf = open_func()
    assert isinstance(conf, dict)

这只是一个例子,而不是现实世界的问题吗?解决这个问题的简单方法是将路径作为参数传递给open_func。这样就不需要完全模拟。您误解了
read\u data
参数-它应该包含文件的内容,而不是名称。我把
read\u data
参数搞错了。感谢您指出这一点。在我的真实代码中,实际上我有两个
Path.open
语句/上下文管理器。它们中的每一个都需要使用不同的文件/内容进行模拟。我该怎么做呢?使用lambda函数是解决这个问题的好方法。在我的实际单元测试中,我多次使用
parametrize
调用原始函数。因此,我考虑通过fixture加载一次原始yaml文件,然后使用
monkeypatch.setitem
在不同的测试中更改其内容。因此,我想知道使用
mocker.mock\u open(read\u data=…)
不是更好的方法吗?在这种情况下,
lambda
函数会是什么样子,即如何模拟
路径的不同文件内容。打开
函数?好的,感谢您更新问题-我在答案中添加了另一部分。感谢您回答我问题的第二部分。我非常感谢你的努力!!您的解决方案工作正常!但是,我没有更改代码中的装置。我需要返回一个
dict
,以便能够通过
monkeypatch.setitem
轻松编辑内容。否则,我将不得不忙于编辑
str
。更改内容后,我使用
file1\u content=yaml.dump(file1\u content)
创建解决方案工作所需的“未解析”内容。