Python mock builtin 'open' in a class using two different files

Question:

I am having trouble figuring out how to mock two file opens in a class when they both use context managers. I know how to do it for one context-managed file using the mock module like this:

@patch('__builtin__.open')
def test_interface_mapping(self, mock_config):
        m = MagicMock(spec=file)
        handle = m.return_value.__enter__.return_value
        handle.__iter__.return_value = ('aa', 'bb')

My problem is how to do this when a class opens two different files in the same call. In my case, the class __init__() preloads the files into two maps. This class is used in other classes. I want to mock the loading of these two files to provide my test data so that the other classes that use the IfAddrConfig object can be tested against my preloaded test file content.

Here’s an example of the class I am struggling with that loads two files in __init__(), both of which I want to mock to load my test injected file contents. getInterfaceMap() is the function that is called frequently so I do not want that to be loading and parsing the files every call, hence the reason for preloading the maps in __init__() once.

class IfAddrConfig(object):
    def __init__(self):
        # Initialize the static maps once since they require file operations
        # that we do not want to be calling every time getInterfaceMap() is used
        self.settings_map = self.loadSettings()
        self.config_map = self.loadConfig()

    def loadConfig(self):
        config_map = defaultdict(dict)
        with open(os.path.join('some_path.cfg'), 'r') as stream:
            for line in stream:
                # Parse line and build up config_map entries
        return config_map

    def loadSettings(self):
        settings_map = {}
        with open('another_path.cfg', 'r') as stream:
            for line in stream:
                # Parse line and build up settings_map entries
        return settings_map

    def getInterfaceMap(self, interface):
        # Uses both the settings and config maps to finally create a composite map
        # that is returned to called
        interface_map = {}
        for values in self.config_map.values():
            # Accesss self.settings_map and combine/compare entries with
            # self.config_map values to build new composite mappings that
            # depend on supplied interface value
        return interface_map
Asked By: chromeeagle

||

Answers:

You’d create two ‘file’ mocks, and mock open to return these in sequence as open() is called. The side_effect attribute lets you do just that:

@patch('__builtin__.open')
def test_interface_mapping(self, mock_open):
    handle1 = MagicMock('file1').__enter__.return_value
    handle1.__iter__.return_value = ('aa', 'bb')
    handle2 = MagicMock('file2').__enter__.return_value
    handle2.__iter__.return_value = ('foo', 'bar')
    mock_open.return_value.side_effect = (handle1, handle2)

The mocked open() call returns first handle1 when called, then handle2. Either object then responds to __enter__() being called with a mock that returns a given tuple for the __iter__ call.

Answered By: Martijn Pieters

You must use side_effect attribute of your patched open object (mock_open) and don’t forget to set the return_value for __exit__ method.

@patch('__builtin__.open', spec=open)
def test_interface_mapping(self, mock_open):
    handle1 = MagicMock()
    handle1.__enter__.return_value.__iter__.return_value = ('aa', 'bb')
    handle1.__exit__.return_value=False
    handle2 = MagicMock()
    handle2.__enter__.return_value.__iter__.return_value = ('AA', 'BB')
    handle2.__exit__.return_value=False
    mock_open.side_effect = (handle1, handle2)
    with open("ppp") as f:
        self.assertListEqual(["aa","bb"],[x for x in f])
    with open("ppp") as f:
        self.assertListEqual(["AA","BB"],[x for x in f])

[EDIT]
I found a much more elegant way to do it Mock builtin 'open" function when used in contextlib

So you can rewrote test like

@patch('__builtin__.open', new_callable=mock_open, read_data="aanbb")
def test_interface_mapping_new(self, mo):
    handlers = (mo.return_value,mock_open(read_data="AAnBB").return_value,)
    mo.side_effect = handlers
    with open("ppp") as f:
        self.assertEqual("aanbb",f.read())
    with open("ppp") as f:
        self.assertEqual("AAnBB",f.read())

And from python 3.4 you can use also readline(), readlines() without mocking anything else.

Answered By: Michele d'Amico

If you need much more control over file content you can use a wrapper function.
It substitutes the content of a file according to the filename as the original open does.

import unittest.mock as mock


def my_open(filename):
    if filename == 'file.txt':
        content = "text filencontent"
    elif filename == 'second.txt':
        content = 'foobar'
    else:
        raise FileNotFoundError(filename)
    file_object = mock.mock_open(read_data=content).return_value
    file_object.__iter__.return_value = content.splitlines(True)
    return file_object

In the elif chain you set “file contents” for each existing file path.

Tests:

# standalone
open_patch = mock.patch('__main__.open', new=my_open)
open_patch.start()

file = open('file.txt')
assert file.read() == "text filencontent"
file.close()

open_patch.stop()

#with statement
with mock.patch('__main__.open', new=my_open):
    with open('second.txt') as file:
        assert file.read() == 'foobar'

    # as iterable
    with open('file.txt') as file:
        assert ['text filen', 'content'] == list(file)

# function decorator
@mock.patch('__main__.open', new=my_open)
def test_patched_open():
    with open('second.txt') as file:
        assert file.readline() == 'foobar'

test_patched_open()
Answered By: warownia1

You can use this in place of mock_open in a patch…

def get_mock_open(files: dict[str, str]):
    def open_mock(filename, *args, **kwargs):
        for expected_filename, content in files.items():
            if filename == expected_filename:
                return mock_open(read_data=content).return_value
        raise FileNotFoundError('(mock) Unable to open {filename}')
    return MagicMock(side_effect=open_mock)

Use

Pass in a dictionary of filename: content, like this…

def test_multiple_opens():
    content1 = 'abc'
    content2 = 'somenlinesncontent'
    files = {'file1': content1,
             'file2': content2}

    with patch('builtins.open', get_mock_open(files)) as open_mock:
        with open('file1', 'r') as file:
            result1 = file.read()
        with open('file2', 'r') as file:
            result2 = file.read()

    assert result1 == content1
    assert result2 == content2
    open_mock.assert_called_with('file2', 'r')
Answered By: Chris Collett

A bit late here but here is a fixture that will let you do it in a much clearer and modern way thanks to mocker on more recent python version:

@fixture()
def mock_files(mocker, request):
    if not hasattr(request, "param"):
        setattr(request, "param", [""])
    if not isinstance(request.param, list):
        request.param = [request.param]

    mock_files = mocker.patch("builtins.open", mock_open(read_data=request.param[0]))

    if len(request.param) > 1:
        mock_files.side_effect = (mock_open(read_data=param).return_value for param in request.param)

    yield mock_files
Answered By: Milan