Home > Mobile >  How do I properly test python function doing file operation?
How do I properly test python function doing file operation?

Time:09-30

I have a function that does some file operations and makes an entry for that IP to /etc/hosts file for DNS resolution

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}"
    )
  • I want to test that there is indeed an IP entry in the file that I made.
  • and that there is only 1 IP address that maps to reg_svc_name

I found how to mock open(). I have this so far but not sure how to check for above two cases:

@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??
    

CodePudding user response:

Solution 1

Don't mock the open functionality because we want it to actually update a file that we can check. Instead, intercept it and open a test file instead of the actual file used in the source code. Here, we will use tmp_path to create a temporary file to be updated for the test.

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

Output

$ 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

If you're interested, you can display the temporary dummy file too to see the result of the process:

$ 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

Solution 2

Mock open as well as the .write operation. Once mocked, see all the calls to the mocked .write via call_args_list. This isn't recommended as it would feel like we are writing a change-detector test which is tightly coupled to how the source code was implemented line by line rather than checking the behavior.

  • Related