How to mock open file and raise if path does not exists?

159 views Asked by At

I'am facing a problem when testing a function that reads the first line of a file and raises an Exception when the path of the file doesn't exist.

Current code:

from unittest.mock import patch, mock_open
from pytest import raises
from os.path import exists

def read_from_file(file_path):
    if not exists(file_path):
        raise Exception("File does not exists!")
    with open(file_path, "r") as f:
        return f.read().splitlines()[0]

@patch("builtins.open", new_callable=mock_open, read_data="Correct string\nWrong string\nWrong string")
@patch("os.path.exists", return_value=True)
def test_read_file_and_returns_the_correct_string_with_multiple_lines(mock_os, mock_file):
    result = read_from_file("xyz")
    mock_file.assert_called_once_with("xyz", "r")
    assert result == "Correct string"

@patch("builtins.open", new_callable=mock_open, read_data="Correct string")
@patch("os.path.exists", return_value=False)
def test_throws_exception_when_file_doesnt_exist(mock_os, mock_file):
    with raises(Exception):
        read_from_file("xyz")

The decorators @patch("os.path.exists", return_value=True) and @patch("os.path.exists", return_value=False) seems to have no effect in both tests.

How can I mock the existence of a file?

2

There are 2 answers

0
elmiomar On BEST ANSWER

The error arises because the read_from_file directly imports exists from os.path and uses it, but the mock targets os.path.exists, which does not affect the direct import. Hence, the real function gets executed instead of the mock.

Change read_from_file to this:

import os.path

def read_from_file(file_path):
    if not os.path.exists(file_path):
        raise Exception("File does not exist!")
    with open(file_path, "r") as f:
        return f.read().splitlines()[0]
2
Iskander14yo On

EDIT. Bad answer, @elmiomar code is correct to me.

The problem you're facing is related to the order of the @patch decorators. The decorators are applied from the innermost to the outermost. So, in your tests, the mock_file argument corresponds to builtins.open, and the mock_os argument corresponds to os.path.exists.

When you mock multiple objects, the mocked objects are passed to the test function in the order the patch decorators appear. In your case, you should switch the order of the mock parameters in the function signature to match the decorators.

from unittest.mock import patch, mock_open
from pytest import raises
from os.path import exists

def read_from_file(file_path):
    if not exists(file_path):
        raise Exception("File does not exists!")
    with open(file_path, "r") as f:
        return f.read().splitlines()[0]

@patch("os.path.exists", return_value=True)
@patch("builtins.open", new_callable=mock_open, read_data="Correct string\nWrong string\nWrong string")
def test_read_file_and_returns_the_correct_string_with_multiple_lines(mock_file, mock_os):
    result = read_from_file("xyz")
    mock_file.assert_called_once_with("xyz", "r")
    assert result == "Correct string"

@patch("os.path.exists", return_value=False)
@patch("builtins.open", new_callable=mock_open, read_data="Correct string")
def test_throws_exception_when_file_doesnt_exist(mock_file, mock_os):
    with raises(Exception):
        read_from_file("xyz")