Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to fix mock_open differences in calls but not in end result

Using mock_open, I can capture the data from writes using the with [...] as construct. However, testing that what I have is correct is a little tricky. For example, I can do this:

>>> from mock import mock_open
>>> m = mock_open()
>>> with patch('__main__.open', m, create=True):
...     with open('foo', 'w') as h:
...         h.write('some stuff')
...
>>> m.mock_calls
[call('foo', 'w'),
 call().__enter__(),
 call().write('some stuff'),
 call().__exit__(None, None, None)]
>>> m.assert_called_once_with('foo', 'w')
>>> handle = m()
>>> handle.write.assert_called_once_with('some stuff')

But I want to do compare what I think should have been written to what was. In effect something like this:

>>> expected = 'some stuff'
>>> assert(expected == m.all_that_was_written)

The problem I am facing with call is that different versions of json (2.0.9 vs 1.9) seem to print things differently. No, I cannot just update to the latest json.

The actual error I am getting is this:

E           AssertionError: [call('Tool_000.json', 'w'),
                             call().__enter__(),
                             call().write('['),
                             call().write('\n  '),
                             call().write('"1.0.0"'),
                             call().write(', \n  '),
                             call().write('"2014-02-27 08:58:02"'),
                             call().write(', \n  '),
                             call().write('"ook"'),
                             call().write('\n'),
                             call().write(']'),
                             call().__exit__(None, None, None)] 
            !=
                            [call('Tool_000.json', 'w'),
                             call().__enter__(),
                             call().write('[\n  "1.0.0"'),
                             call().write(', \n  "2014-02-27 08:58:02"'),
                             call().write(', \n  "ook"'),
                             call().write('\n'),
                             call().write(']'),
                             call().__exit__(None, None, None)]

In effects, the calls are different but the end result is the same.

The code I am testing is fairly simple:

with open(get_new_file_name(), 'w') as fp:
    json.dump(lst, fp)

So, creating another method that passes the file pointer seems overkill.

like image 703
Sardathrion - against SE abuse Avatar asked Mar 05 '14 14:03

Sardathrion - against SE abuse


2 Answers

You can patch open() to return StringIO object and then check the contents.

with mock.patch('module_under_test.open', create=True) as mock_open:
    stream = io.StringIO()
    # patching to make getvalue() work after close() or __exit__()
    stream.close = mock.Mock(return_value=None)
    mock_open.return_value = stream

    module_under_test.do_something() # this calls open()

    contents = stream.getvalue()
    assert(contents == expected)

Edit: added patch for stream.close to avoid exception on stream.getvalue().

like image 149
and Avatar answered Sep 25 '22 18:09

and


mock_open is not fully featured yet. It works well if you are mocking files to be read but it does not yet have enough features for testing written files. The question clearly shows this deficiency.

My solution is to not use mock_open if you are testing the written content. Here is the alternative:

import six
import mock
import unittest

class GenTest(unittest.TestCase):
    def test_open_mock(self):
        io = six.BytesIO()
        io_mock = mock.MagicMock(wraps=io)
        io_mock.__enter__.return_value = io_mock
        io_mock.close = mock.Mock() # optional
        with mock.patch.object(six.moves.builtins, 'open', create=True, return_value=io_mock):
            # test using with
            with open('foo', 'w') as h:
                expected = 'some stuff'
                h.write(expected)
            self.assertEquals(expected, io.getvalue())
            # test using file handle directly
            io.seek(0); io.truncate() # reset io
            expected = 'other stuff'
            open('bar', 'w').write(expected)
            self.assertEquals(expected, io.getvalue())
            # test getvalue after close
            io.seek(0); io.truncate() # reset io
            expected = 'closing stuff'
            f = open('baz', 'w')
            f.write(expected)
            f.close()
            self.assertEquals(expected, io.getvalue())

if __name__ == '__main__':
    unittest.main()
like image 38
Pykler Avatar answered Sep 24 '22 18:09

Pykler