如何正确测试执行文件操作的 python 函数?

How do I properly test python function doing file operation?

我有一个函数可以做一些文件操作和 在 /etc/hosts 文件中为该 IP 创建条目以进行 DNS 解析

def add_hosts_entry():
        
    ip_addr = "1.2.3.4"
    HOST_FILE_PATH = "/etc/hosts"
    reg_svc_name = "SVC_NAME"

    try:
        with open(HOST_FILE_PATH, 'r+') as fp:
            lines = fp.readlines()
            fp.seek(0)
            fp.truncate()

            for line in lines:
                if not reg_svc_name in line:
                    fp.write(line)
            fp.write(f"{ip_addr}\t{reg_svc_name}\n")
    except FileNotFoundError as ex:
        LOGGER.error(f"Failed to read file. Details: {repr(ex)}")
        sys.exit(1)
    LOGGER.info(
        f"Successfully made entry in /etc/hosts file:\n{ip_addr}\t{reg_svc_name}"
    )

我找到了如何模拟 open()。 到目前为止我有这个但不确定如何检查以上两种情况:

@pytest.fixture
def mocker_etc_hosts(mocker):
    mocked_etc_hosts_data = mocker.mock_open(read_data=etc_hosts_sample_data)
    mocker.patch("builtins.open", mocked_etc_hosts_data)
    

def test_add_hosts_entry(mocker_etc_hosts):
    with caplog.at_level(logging.INFO):
        registry.add_hosts_entry()
    # how to assert??
    

解决方案 1

不要模拟 open 功能,因为我们希望它实际更新我们可以检查的文件。相反,拦截它并打开一个测试文件而不是源代码中使用的实际文件。在这里,我们将使用tmp_path to create a temporary file更新进行测试。

src.py

def add_hosts_entry():
    ip_addr = "1.2.3.4"
    HOST_FILE_PATH = "/etc/hosts"
    reg_svc_name = "SVC_NAME"

    try:
        with open(HOST_FILE_PATH, 'r+') as fp:
            lines = fp.readlines()
            fp.seek(0)
            fp.truncate()

            for line in lines:
                if not reg_svc_name in line:
                    fp.write(line)
            fp.write(f"{ip_addr}\t{reg_svc_name}\n")
    except FileNotFoundError as ex:
        print(f"Failed to read file. Details: {repr(ex)}")
    else:
        print(f"Successfully made entry in /etc/hosts file:\n{ip_addr}\t{reg_svc_name}")

test_src.py

import pytest

from src import add_hosts_entry


@pytest.fixture
def etc_hosts_content_raw():
    return "some text\nhere\nSVC_NAME\nand the last!\n"


@pytest.fixture
def etc_hosts_content_updated():
    return "some text\nhere\nand the last!\n1.2.3.4\tSVC_NAME\n"


@pytest.fixture
def etc_hosts_file(tmp_path, etc_hosts_content_raw):
    file = tmp_path / "dummy_etc_hosts"
    file.write_text(etc_hosts_content_raw)
    return file


@pytest.fixture
def mocker_etc_hosts(mocker, etc_hosts_file):
    real_open = open

    def _mock_open(file, *args, **kwargs):
        print(f"Intercepted. Would open {etc_hosts_file} instead of {file}")
        return real_open(etc_hosts_file, *args, **kwargs)

    mocker.patch("builtins.open", side_effect=_mock_open)


def test_add_hosts_entry(
    mocker_etc_hosts, etc_hosts_file, etc_hosts_content_raw, etc_hosts_content_updated
):
    assert etc_hosts_file.read_text() == etc_hosts_content_raw
    add_hosts_entry()
    assert etc_hosts_file.read_text() == etc_hosts_content_updated

输出

$ pytest -q -rP
.                                                                                             [100%]
============================================== PASSES ===============================================
_______________________________________ test_add_hosts_entry ________________________________________
--------------------------------------- Captured stdout call ----------------------------------------
Intercepted. Would open /tmp/pytest-of-nponcian/pytest-13/test_add_hosts_entry0/dummy_etc_hosts instead of /etc/hosts
Successfully made entry in /etc/hosts file:
1.2.3.4 SVC_NAME
1 passed in 0.05s

如果您有兴趣,也可以显示临时虚拟文件以查看处理结果:

$ cat /tmp/pytest-of-nponcian/pytest-13/test_add_hosts_entry0/dummy_etc_hosts
some text
here
and the last!
1.2.3.4 SVC_NAME

解决方案 2

模拟open以及.write操作。模拟后,通过 call_args_list 查看对模拟 .write 的所有调用。不推荐这样做,因为感觉我们正在编写一个更改检测器测试,它与源代码的逐行实现方式紧密耦合,而不是检查行为。