Wakaru44
Wakaru44

Reputation: 453

Mocking a file containing JSON data with mock.patch and mock_open

I'm trying to test a method that requires the use of json.load in Python 3.6. And after several attempts, I tried running the test "normally" (with the usual unittest.main() from the CLI), and in the iPython REPL.

Having the following function (simplified for purpose of the example)

def load_metadata(name):
    with open("{}.json".format(name)) as fh:
        return json.load(fh)

with the following test:

class test_loading_metadata(unittest2.TestCase):
    @patch('builtins.open', new_callable=mock_open(read_data='{"disabled":True}'))
    def test_load_metadata_with_disabled(self, filemock):
        result = load_metadata("john")
        self.assertEqual(result,{"disabled":True})
        filemock.assert_called_with("john.json")

The result of the execution of the test file, yields a heart breaking:

TypeError: the JSON object must be str, bytes or bytearray, not 'MagicMock'

While executing the same thing in the command line, gives a successful result.

I tried in several ways (patching with with, as decorator), but the only thing that I can think of, is the unittest library itself, and whatever it might be doing to interfere with mock and patch.

Also checked versions of python in the virtualenv and ipython, the versions of json library.

I would like to know why what looks like the same code, works in one place and doesn't work in the other. Or at least a pointer in the right direction to understand why this could be happening.

Upvotes: 5

Views: 9049

Answers (1)

Martijn Pieters
Martijn Pieters

Reputation: 1121744

json.load() simply calls fh.read(), but fh is not a mock_open() object. It's a mock_open()() object, because new_callable is called before patching to create the replacement object:

>>> from unittest.mock import patch, mock_open
>>> with patch('builtins.open', new_callable=mock_open(read_data='{"disabled":True}')) as filemock:
...     with open("john.json") as fh:
...         print(fh.read())
...
<MagicMock name='open()().__enter__().read()' id='4420799600'>

Don't use new_callable, you don't want your mock_open() object to be called! Just pass it in as the new argument to @patch() (this is also the second positional argument, so you can leave off the new= here):

@patch('builtins.open', mock_open(read_data='{"disabled":True}'))
def test_load_metadata_with_disabled(self, filemock):

at which point you can call .read() on it when used as an open() function:

>>> with patch('builtins.open', mock_open(read_data='{"disabled":True}')) as filemock:
...     with open("john.json") as fh:
...         print(fh.read())
...
{"disabled":True}

The new argument is the object that'll replace the original when patching. If left to the default, new_callable() is used instead. You don't want new_callable() here.

Upvotes: 5

Related Questions