Skip to content

Latest commit

 

History

History
202 lines (140 loc) · 7.2 KB

reading-writing-on-files.md

File metadata and controls

202 lines (140 loc) · 7.2 KB

Reading / Writing on Files

Topics on this page:

Reading Files

Let's say I have a method that returns the number of lines from a file.

class FileReader:

    @staticmethod
    def count_lines(file_path):
        with open(file_path, 'r') as _file:
            file_content_list = _file.readlines()
            print(file_content_list)
            return len(file_content_list)

But I don't want to open a real file. I wanna mock the opening and the content of the file. This is elegantly possible with mock_open().

This replace the use of open().

This works with both open() called directly like this:

file = open('file/path', r)

and with a context manager like this:

with open('file/path', r) as _file:
    # ...

Using mock_open

See the official documentation for further detail.

We are going to use patch with parameter new=mock_open(), so the target is replaced with a mock_open object.

mock_open() has a parameter called read_data that is a string for the read(), readline() and readlines() methods of the file opened.

Here is how to mock the file opening and reading with a context manager.

with patch('__main__.open', new=mock_open(read_data='Fooooo')) as _file:
    # do your call to path 'foo/bar'
    _file.assert_called_once_with('foo/bar', 'r')

FAQ about this code

Whatafuck is a context manager? It's basically using the syntax with ..... as variable:. More details here.

Whatafuck is that __main__.open? That, my friend, is the reference for where the object open is being looked up. In the example above, however, that __main__.open is just for illustration, because I wasn't mocking any specific open(). More details in the official doc.

Whatafuck is that assert_called_once_with? It makes an assertion if the object was called only one time with the respective parameters. Check it out here.

I bet my ass am pretty sure that you are fucking ready to test our method count_lines_from_file() now.

So let's check out the test code.

import unittest
from unittest.mock import patch, mock_open

from examples.count_lines.file_reader import FileReader


class TestReadFiles(unittest.TestCase):
    def test_count_lines(self):
        file_content_mock = """Hello World!!
Hello World is in a file.
A mocked file.
He is not real.
But he think he is.
He doesn't know he is mocked"""
        fake_file_path = 'file/path/mock'

        with patch('examples.count_lines.file_reader.open'.format(__name__),
                   new=mock_open(read_data=file_content_mock)) as _file:
            actual = FileReader().count_lines(fake_file_path)
            _file.assert_called_once_with(fake_file_path, 'r')

        expected = len(file_content_mock.split('\n'))
        self.assertEqual(expected, actual)

See the source code

Writing on Files

Let's make the simplest. A method that receives a message and write it on a file given a specific file_path.

class FileWriter:
    @staticmethod
    def write(file_path, content):
        with open(file_path, 'w') as file:
            file.write(content)

To mock the opening file and writing content on it, we can use mock_open(). The mock for this object will be similar to what we've previously seen in Reading Files, with the exception that we don't need to pass the parameter read_data in mock_open() because we are not retrieving data from the file.

How the assertion will look like?

To answer this question, we need to ask ourselves:

What do we want to test in this function?

A nice test case, in my delusional opinion, will test if a specific file_path was called on open(), with a specific open mode, and if a specific content was written in the file.

The Mock object implements assertions that could help us testing that. The one that will be useful to us is MockObject.assert_called_once_with().

Let's take a look on the test?

import unittest
from unittest.mock import patch, mock_open

from examples.write_on_file.file_writer import FileWriter


class TestFileWriter(unittest.TestCase):
    def test_file_writer(self):
        fake_file_path = "fake/file/path"
        content = "Message to write on file to be written"
        with patch('examples.write_on_file.file_writer.open', mock_open()) as mocked_file:
            FileWriter().write(fake_file_path, content)

            # assert if opened file on write mode 'w'
            mocked_file.assert_called_once_with(fake_file_path, 'w')

            # assert if write(content) was called from the file opened
            # in another words, assert if the specific content was written in file
            mocked_file().write.assert_called_once_with(content)

In this test, we mock the opening of the file with a context manager. The variable mocked_file is the mocked opened file.

  • examples.write_on_file.file_writer.open is the reference for where the object open() is being looked up.

Inside the context manager, we can:

  • call our actual method FileWriter().write(fake_file_path, content)
  • assert if mocked_file was opened with the specific file path: fake_file_path, and write open mode: w
  • assert if write() from mocked_file was called with the parameter content

See the source code.

Congratulations

You've just learned how to mock reading and writing on a file without even opening a real one.

That's a huge reason to celebrate. Congrats!!!

Let's celebrate

What about now?

Credits