1

I try test a function which reads a file and returns the content of the file or returns none if the file is not found.

def read_yaml_from_cwd(file: str) -> Dict:
    """[reads a yaml file from current working directory]
    Args:
        file ([type]): [.yaml or .yml file]
    Returns:
        [type]: [Dictionary]
    """
    path = os.path.join(Path.cwd().resolve(), file)
    if os.path.isfile(path):
        with open(path) as f:
            content = yaml.load(f, Loader=SafeLoader)
            return content
    else:
        return None

This is my test:

from unittest import mock, TestCase
from project import fs

class TextExamples(TestCase):

    def test_read_yaml_from_cwd():
        with mock.patch('os.listdir') as mocked_listdir:
            mocked_listdir.return_value = ['test-config.yml']
            val = fs.read_yaml_from_cwd("false-config.yml")
            assert val == None
            val2 = fs.read_yaml_from_cwd("false-config.yml")
            assert val2 != None

I guess I am fundamentally doing something wrong with these tests and what these mocks do. Can somebody help me with this?

Data Mastery
  • 1,555
  • 4
  • 18
  • 60
  • Why are you mocking `os.listdir` when you are actually calling `os.path.join`, `os.path.isfile` & `open` in the function? You also need to mock `yaml.load` – rdas Nov 27 '21 at 15:41
  • If you mock away all of this, there's not much that remains to actually be tested, though if you just want to test if it returns `None` if the file does not exist I guess that would be sufficient. If you want to test more of the fs related stuff you may want to use something like pyfakefs instead (Disclaimer - I'm a contributor to pyfakefs). – MrBean Bremen Nov 27 '21 at 16:02
  • Hm okay so how would you then write a test for this function? – Data Mastery Nov 27 '21 at 16:43

1 Answers1

3

One possibility to test this is to patch both os.path.isfile and open. To patch open, there is already a special mock function, mock_open, which gives you the possibilty to set the contents of the mocked file. This means that you don't have to mock yaml.load, as this will return the mocked file content. This could look something like:

from unittest import mock, TestCase
from unittest.mock import mock_open

class YamlTest(TestCase):

    @mock.patch("builtins.open", mock_open(read_data="data"))
    @mock.patch("os.path.isfile")
    def test_read_yaml_from_cwd(self, patched_isfile):
        # valid file case
        patched_isfile.return_value = True
        result = read_yaml_from_cwd("some_file.yaml")
        self.assertEqual("data", result)

        # invalid file case
        patched_isfile.return_value = False
        result = read_yaml_from_cwd("some_file.yaml")
        self.assertEqual(None, result)

In this case you test that the function returns the file content if you pass a valid file name, and None for an invalid file name, which is probably all you want to test here.

For completeness, and because I mentioned it in the comments: using pyfakefs instead would replace the file system with a fake file system, which you can handle like a real filesystem, in this case it could look like:

from pyfakefs import fake_filesystem_unittest

class YamlTest(fake_filesystem_unittest.TestCase):

    def setUp(self) -> None:
        self.setUpPyfakefs()
        self.fs.create_file("some_file.yaml", contents="data")

    def test_read_yaml_from_cwd(self):
        # valid file case
        result = read_yaml_from_cwd("some_file.yaml")
        self.assertEqual("data", result)

        # invalid file case
        result = read_yaml_from_cwd("non_existing.yaml")
        self.assertEqual(None, result)

This makes sense if you have many filesystem related tests, though in your case this would probably be overkill.

Disclaimer: I'm a contributor to pyfakefs.

MrBean Bremen
  • 14,916
  • 3
  • 26
  • 46