diff -r de5f5648be2d Doc/library/unittest.mock.rst --- a/Doc/library/unittest.mock.rst Tue Mar 15 17:24:13 2016 +0100 +++ b/Doc/library/unittest.mock.rst Tue Mar 15 21:46:11 2016 +0200 @@ -2115,6 +2115,19 @@ >>> m.assert_called_once_with('foo') >>> assert result == 'bibble' +A :func:`mock_open` object also acts as a mapping of file names to mock objects: + + >>> m = mock_open() + >>> m['foo'].read_data = 'FOO' + >>> m['bar'].read_data = 'BAR' + >>> with patch('__main__.open', m): + ... with open('foo') as h: + ... foo_contents = h.read() + ... with open('bar') as h: + ... bar_contents = h.read() + ... + >>> assert foo_contents == 'FOO' + >>> assert bar_contents == 'BAR' .. _auto-speccing: diff -r de5f5648be2d Lib/unittest/mock.py --- a/Lib/unittest/mock.py Tue Mar 15 17:24:13 2016 +0100 +++ b/Lib/unittest/mock.py Tue Mar 15 21:46:11 2016 +0200 @@ -17,6 +17,8 @@ 'NonCallableMock', 'NonCallableMagicMock', 'mock_open', + 'MockOpen', + 'FileLikeMock', 'PropertyMock', ) @@ -30,6 +32,7 @@ import builtins from types import ModuleType from functools import wraps, partial +from io import TextIOWrapper, StringIO, BytesIO _builtins = {name for name in dir(builtins) if not name.startswith('_')} @@ -2289,91 +2292,179 @@ file_spec = None -def _iterate_read_data(read_data): - # Helper for mock_open: - # Retrieve lines from read_data via a generator so that separate calls to - # readline, read, and readlines are properly interleaved - sep = b'\n' if isinstance(read_data, bytes) else '\n' - data_as_list = [l + sep for l in read_data.split(sep)] - - if data_as_list[-1] == sep: - # If the last line ended in a newline, the list comprehension will have an - # extra entry that's just a newline. Remove this. - data_as_list = data_as_list[:-1] - else: - # If there wasn't an extra newline by itself, then the file being - # emulated doesn't have a newline to end the last line remove the - # newline that our naive format() added - data_as_list[-1] = data_as_list[-1][:-1] - - for line in data_as_list: - yield line - - -def mock_open(mock=None, read_data=''): - """ - A helper function to create a mock to replace the use of `open`. It works - for `open` called directly or used as a context manager. - - The `mock` argument is the mock object to configure. If `None` (the - default) then a `MagicMock` will be created for you, with the API limited - to methods or attributes available on standard file handles. - - `read_data` is a string for the `read` methoddline`, and `readlines` of the - file handle to return. This is an empty string by default. - """ - def _readlines_side_effect(*args, **kwargs): - if handle.readlines.return_value is not None: - return handle.readlines.return_value - return list(_state[0]) - - def _read_side_effect(*args, **kwargs): - if handle.read.return_value is not None: - return handle.read.return_value - return type(read_data)().join(_state[0]) - - def _readline_side_effect(): - if handle.readline.return_value is not None: - while True: - yield handle.readline.return_value - for line in _state[0]: - yield line - - - global file_spec - if file_spec is None: - import _io - file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO)))) - - if mock is None: - mock = MagicMock(name='open', spec=open) - - handle = MagicMock(spec=file_spec) - handle.__enter__.return_value = handle - - _state = [_iterate_read_data(read_data), None] - - handle.write.return_value = None - handle.read.return_value = None - handle.readline.return_value = None - handle.readlines.return_value = None - - handle.read.side_effect = _read_side_effect - _state[1] = _readline_side_effect() - handle.readline.side_effect = _state[1] - handle.readlines.side_effect = _readlines_side_effect - - def reset_data(*args, **kwargs): - _state[0] = _iterate_read_data(read_data) - if handle.readline.side_effect == _state[1]: - # Only reset the side effect if the user hasn't overridden it. - _state[1] = _readline_side_effect() - handle.readline.side_effect = _state[1] - return DEFAULT - - mock.side_effect = reset_data - mock.return_value = handle - return mock + +class FileLikeMock(NonCallableMock): + """Acts like a file object returned from open().""" + def __init__(self, name=None, read_data='', *args, **kws): + kws.update({'spec': TextIOWrapper, }) + super(FileLikeMock, self).__init__(*args, **kws) + self.mode = None + self.__is_closed = False + self.read_data = read_data + self.close.side_effect = self._close + self.__contents.seek(0) + + self.__enter__ = Mock(side_effect=self._enter) + self.__exit__ = Mock(side_effect=self._exit) + + if name is not None: + self.name = name + + @property + def closed(self): + # pylint: disable=missing-docstring + return self.__is_closed + + @property + def read_data(self): + """Bypass read function to access the contents of the file. + + This property should be used for testing purposes. + """ + return self.__contents.getvalue() + + @read_data.setter + def read_data(self, contents): + # pylint: disable=missing-docstring + # pylint: disable=attribute-defined-outside-init + if isinstance(contents, str): + self.__contents = StringIO() + else: + self.__contents = BytesIO() + + # Constructing a cStrinIO object with the input string would result + # in a read-only object, so we write the contents after construction. + self.__contents.write(contents) + + # Set tell/read/write/etc side effects to access the new contents + # object. + self.tell._mock_wraps = self.__contents.tell + self.seek._mock_wraps = self.__contents.seek + self.read._mock_wraps = self.__contents.read + self.readline._mock_wraps = self.__contents.readline + self.readlines._mock_wraps = self.__contents.readlines + self.write._mock_wraps = self.__contents.write + self.writelines._mock_wraps = self.__contents.writelines + + def __iter__(self): + return iter(self.__contents) + + def set_properties(self, path, mode): + """Set file's properties (name and mode). + + This function is also in charge of swapping between textual and + binary streams. + """ + self.name = path + self.mode = mode + + if 'b' in self.mode: + if not isinstance(self.read_data, bytes): + self.read_data = bytes(self.read_data, encoding='utf8') + else: + if not isinstance(self.read_data, str): + self.read_data = str(self.read_data, encoding='utf8') + + def reset_mock(self, visited=None): + """Reset the default tell/read/write/etc side effects.""" + # In some versions of the mock library, `reset_mock` takes an argument + # and in some it doesn't. We try to handle all situations. + if visited is not None: + super(FileLikeMock, self).reset_mock(visited) + else: + super(FileLikeMock, self).reset_mock() + + # Reset contents and tell/read/write/close side effects. + self.read_data = '' + self.close.side_effect = self._close + + def _enter(self): + """Reset the position in buffer whenever entering context.""" + self.__contents.seek(0) + + return self + + def _exit(self, exception_type, exception, traceback): + """Close file when exiting context.""" + # pylint: disable=unused-argument + self.close() + + def _close(self): + """Mark file as closed (used for side_effect).""" + self.__is_closed = True + + +class MockOpen(Mock): + """A mock for the open() builtin function.""" + def __init__(self, read_data='', *args, **kws): + kws.update({'spec': open, 'name': open.__name__, }) + super(MockOpen, self).__init__(*args, **kws) + self.__files = {} + self.__read_data = read_data + + def _mock_call(self, path, mode='r', *args, **kws): + original_side_effect = self._mock_side_effect + + if path in self.__files: + self._mock_return_value = self.__files[path] + self._mock_side_effect = self._mock_return_value.side_effect + + try: + child = super(MockOpen, self)._mock_call(path, mode, *args, **kws) + finally: + # Reset the side effect after each call so that the next call to + # open() won't cause the same side_effect. + self._mock_side_effect = original_side_effect + + # Consecutive calls to open() set `return_value` to the last file mock + # created. If the paths differ (and child isn't a newly-created mock, + # evident by its name attribute being unset) we create a new file mock + # instead of returning to previous one. + if not isinstance(child.name, Mock) and path != child.name: + child = self._get_child_mock(_new_name='()', name=path) + self.__files[path] = child + + child.set_properties(path, mode) + + if path not in self.__files: + self.__files[path] = child + + self._mock_return_value = child + return child + + def __getitem__(self, path): + return self.__files.setdefault(path, self._get_child_mock(name=path)) + + def __setitem__(self, path, value): + value.__enter__ = lambda self: self + value.__exit__ = lambda self, *args: None + self.__files[path] = value + + def reset_mock(self, visited=None): + # See comment in `FileLikeMock.reset_mock`. + if visited is not None: + super(MockOpen, self).reset_mock(visited) + else: + super(MockOpen, self).reset_mock() + + self.__files = {} + self.__read_data = '' + + def _get_child_mock(self, **kws): + """Create a new FileLikeMock instance. + + The new mock will inherit the parent's side_effect and read_data + attributes. + """ + kws.update({ + '_new_parent': self, + 'side_effect': self._mock_side_effect, + 'read_data': self.__read_data, + }) + return FileLikeMock(**kws) + + +mock_open = MockOpen class PropertyMock(Mock): diff -r de5f5648be2d Lib/unittest/test/testmock/testmock.py --- a/Lib/unittest/test/testmock/testmock.py Tue Mar 15 17:24:13 2016 +0100 +++ b/Lib/unittest/test/testmock/testmock.py Tue Mar 15 21:46:11 2016 +0200 @@ -1404,22 +1404,11 @@ f2_data = f2.read() self.assertEqual(f1_data, f2_data) - def test_mock_open_write(self): - # Test exception in file writing write() - mock_namedtemp = mock.mock_open(mock.MagicMock(name='JLV')) - with mock.patch('tempfile.NamedTemporaryFile', mock_namedtemp): - mock_filehandle = mock_namedtemp.return_value - mock_write = mock_filehandle.write - mock_write.side_effect = OSError('Test 2 Error') - def attempt(): - tempfile.NamedTemporaryFile().write('asd') - self.assertRaises(OSError, attempt) - def test_mock_open_alter_readline(self): mopen = mock.mock_open(read_data='foo\nbarn') - mopen.return_value.readline.side_effect = lambda *args:'abc' - first = mopen().readline() - second = mopen().readline() + mopen.return_value.readline.return_value = 'abc' + first = mopen('/path/to/file').readline() + second = mopen('/path/to/file').readline() self.assertEqual('abc', first) self.assertEqual('abc', second) diff -r de5f5648be2d Lib/unittest/test/testmock/testmockopen.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/unittest/test/testmock/testmockopen.py Tue Mar 15 21:46:11 2016 +0200 @@ -0,0 +1,594 @@ +"""Test cases for the mocks module.""" + +import sys +import unittest +from functools import wraps +from unittest.mock import patch, call, NonCallableMock, MockOpen, FileLikeMock, DEFAULT + +OPEN = 'builtins.open' + + +@patch(OPEN, new_callable=MockOpen) +class TestOpenSingleFiles(unittest.TestCase): + """Test the MockOpen and FileLikeMock classes for single file usage.""" + def test_read(self, mock_open): + """Check effects of reading from an empty file.""" + handle = open('/path/to/file', 'r') + self.assertFalse(handle.closed) + self.assertEqual('/path/to/file', handle.name) + self.assertEqual('r', handle.mode) + self.assertEqual(0, handle.tell()) + + text = handle.read() + self.assertEqual(0, handle.tell()) + self.assertEqual('', text) + + handle.close() + self.assertTrue(handle.closed) + + self.assertEqual(handle, mock_open.return_value) + mock_open.assert_called_once_with('/path/to/file', 'r') + handle.read.assert_called_once_with() + handle.close.assert_called_once_with() + + def test_write(self, mock_open): + """Check effects of writing to a file.""" + handle = open('/path/to/file', 'w') + self.assertFalse(handle.closed) + self.assertEqual('/path/to/file', handle.name) + self.assertEqual('w', handle.mode) + self.assertEqual(0, handle.tell()) + + handle.write("some text\n") + self.assertEqual(len("some text\n"), handle.tell()) + handle.write('More text!') + self.assertEqual( + len('some text\n') + len('More text!'), + handle.tell()) + + handle.close() + self.assertTrue(handle.closed) + + self.assertEqual(handle, mock_open.return_value) + mock_open.assert_called_once_with('/path/to/file', 'w') + self.assertEqual( + [call('some text\n'), call('More text!'), ], + handle.write.mock_calls) + self.assertEqual('some text\nMore text!', handle.read_data) + handle.close.assert_called_once_with() + + def test_context_manager(self, mock_open): + """Check calls made when `open` is used as a context manager.""" + with open('/path/to/file', 'r') as handle: + self.assertFalse(handle.closed) + self.assertEqual('/path/to/file', handle.name) + self.assertEqual('r', handle.mode) + self.assertEqual(0, handle.tell()) + + mock_open.assert_has_calls([ + call('/path/to/file', 'r'), + call().__enter__(), + call().tell(), + call().__exit__(None, None, None), + call().close(), + ]) + + def test_read_as_context_manager(self, mock_open): + """Check effects of reading from an empty file using a context + manager. + """ + with open('/path/to/file', 'r') as handle: + text = handle.read() + self.assertEqual(0, handle.tell()) + self.assertEqual('', text) + + self.assertTrue(handle.closed) + self.assertEqual(handle, mock_open.return_value) + mock_open.assert_called_once_with('/path/to/file', 'r') + handle.read.assert_called_once_with() + handle.close.assert_called_once_with() + + def test_write_as_context_manager(self, mock_open): + """Check effects of writing to a file using a context manager.""" + with open('/path/to/file', 'w') as handle: + handle.write("some text\n") + self.assertEqual(len("some text\n"), handle.tell()) + handle.write('More text!') + self.assertEqual( + len('some text\n') + len('More text!'), + handle.tell()) + + self.assertTrue(handle.closed) + self.assertEqual(handle, mock_open.return_value) + mock_open.assert_called_once_with('/path/to/file', 'w') + self.assertEqual( + [call('some text\n'), call('More text!'), ], + handle.write.mock_calls) + self.assertEqual('some text\nMore text!', handle.read_data) + handle.close.assert_called_once_with() + + def test_seek(self, _): + """Check calling seek().""" + with open('/path/to/file', 'w+') as handle: + handle.write('There\'s no place like home') + handle.seek(len('There\'s ')) + self.assertEqual('no place like home', handle.read()) + + def test_set_contents(self, mock_open): + """Check setting file's contents before reading from it.""" + contents = [ + 'This is the first line', + 'This is the second', + 'This is the third line', + ] + + # We even allow adding contents to the file incrementally. + mock_open.return_value.read_data = '\n'.join(contents[:-1]) + mock_open.return_value.read_data += '\n' + contents[-1] + + with open('/path/to/file', 'r') as handle: + data = handle.read() + + handle.read.assert_called_once_with() + self.assertEqual('\n'.join(contents), data) + + def test_read_size(self, mock_open): + """Check reading a certain amount of bytes from the file.""" + mock_open.return_value.read_data = '0123456789' + with open('/path/to/file', 'r') as handle: + self.assertEqual('0123', handle.read(4)) + self.assertEqual('4567', handle.read(4)) + self.assertEqual('89', handle.read()) + + def test_different_read_calls(self, mock_open): + """Check that read/readline/readlines all work in sync.""" + contents = [ + 'Now that she\'s back in the atmosphere', + 'With drops of Jupiter in her hair, hey, hey, hey', + 'She acts like summer and walks like rain', + 'Reminds me that there\'s a time to change, hey, hey, hey', + 'Since the return from her stay on the moon', + 'She listens like spring and she talks like June, hey, hey, hey', + ] + + mock_open.return_value.read_data = '\n'.join(contents) + with open('/path/to/file', 'r') as handle: + first_line = handle.read(len(contents[0]) + 1) + second_line = handle.readline() + third_line = handle.read(len(contents[2]) + 1) + rest = handle.readlines() + + self.assertEqual(contents[0] + '\n', first_line) + self.assertEqual(contents[1] + '\n', second_line) + self.assertEqual(contents[2] + '\n', third_line) + self.assertEqual('\n'.join(contents[3:]), ''.join(rest)) + + def test_different_write_calls(self, _): + """Check multiple calls to write and writelines.""" + contents = [ + 'They paved paradise', + 'And put up a parking lot', + 'With a pink hotel, a boutique', + 'And a swinging hot SPOT', + 'Don\'t it always seem to go', + 'That you don\'t know what you\'ve got', + '\'Til it\'s gone', + 'They paved paradise', + 'And put up a parking lot', + ] + + with open('/path/to/file', 'w') as handle: + handle.write(contents[0] + '\n') + handle.write(contents[1] + '\n') + handle.writelines(line + '\n' for line in contents[2:4]) + handle.write(contents[4] + '\n') + handle.writelines(line + '\n' for line in contents[5:]) + + self.assertEqual(contents, handle.read_data.splitlines()) + + def test_iteration(self, mock_open): + """Test iterating over the file handle.""" + contents = [ + "So bye, bye, Miss American Pie\n", + "Drove my Chevy to the levee but the levee was dry\n", + "And them good ole boys were drinking whiskey 'n rye\n", + "Singin' this'll be the day that I die\n", + 'This\'ll be the day that I die', + ] + + mock_open.return_value.read_data = ''.join(contents) + with open('/path/to/file', 'r') as handle: + for (i, line) in enumerate(handle): + self.assertEqual(contents[i], line) + + def test_getitem_after_call(self, mock_open): + """Retrieving a handle after the call to open() should give us the same + object. + """ + with open('/path/to/file', 'r') as handle: + pass + + self.assertEqual(handle, mock_open['/path/to/file']) + + def test_setting_custom_mock(self, mock_open): + """Check 'manually' setting a mock for a file.""" + custom_mock = NonCallableMock() + mock_open['/path/to/file'] = custom_mock + + # Make sure other files aren't affected. + self.assertIsInstance(open('/path/to/other_file', 'r'), FileLikeMock) + + # Check with a regular call. + self.assertEqual(custom_mock, open('/path/to/file', 'r')) + + # Check as a context manager. + custom_mock.read.side_effect = IOError() + custom_mock.write.side_effect = IOError() + with open('/path/to/file') as handle: + self.assertIs(custom_mock, handle) + self.assertRaises(IOError, handle.read) + self.assertRaises(IOError, handle.write, 'error') + + +@patch(OPEN, new_callable=MockOpen) +class TestMultipleCalls(unittest.TestCase): + """Test multiple calls to open().""" + def test_read_then_write(self, _): + """Accessing the same file should handle the same object. + + Reading from a file after writing to it should give us the same + contents. + """ + with open('/path/to/file', 'w') as first_handle: + first_handle.write('Ground control to Major Tom') + + with open('/path/to/file', 'r') as second_handle: + contents = second_handle.read() + + self.assertEqual(first_handle, second_handle) + self.assertEqual('Ground control to Major Tom', contents) + + def test_access_different_files(self, mock_open): + """Check access to different files with multiple calls to open().""" + first_handle = mock_open['/path/to/first_file'] + second_handle = mock_open['/path/to/second_file'] + + # Paths should be set when created, if possible. + # Note this isn't the case when not specifically instantiating a file + # mock (eg., by using `return_value` instead). + self.assertEqual('/path/to/first_file', first_handle.name) + self.assertEqual('/path/to/second_file', second_handle.name) + + first_handle.read_data = 'This is the FIRST file' + second_handle.read_data = 'This is the SECOND file' + + with open('/path/to/first_file', 'r') as handle: + self.assertEqual('/path/to/first_file', handle.name) + self.assertEqual('This is the FIRST file', handle.read()) + + with open('/path/to/second_file', 'r') as handle: + self.assertEqual('/path/to/second_file', handle.name) + self.assertEqual('This is the SECOND file', handle.read()) + + # return_value is set to the last handle returned. + self.assertEqual(second_handle, mock_open.return_value) + + self.assertEqual('r', first_handle.mode) + self.assertEqual('r', second_handle.mode) + first_handle.read.assert_called_once_with() + second_handle.read.assert_called_once_with() + + def test_return_value(self, mock_open): + """Check that `return_value` always returns the last file mock.""" + with open('/path/to/first_file', 'r'): + pass + + with open('/path/to/second_file', 'r') as handle: + self.assertEqual(handle, mock_open.return_value) + + +@patch(OPEN, new_callable=MockOpen) +class TestSideEffects(unittest.TestCase): + """Test setting the side_effect attribute in various situations.""" + def test_error_on_open(self, mock_open): + """Simulate error openning a file.""" + mock_open.side_effect = IOError() + + self.assertRaises(IOError, open, '/not/there', 'r') + + def test_error_on_any_open(self, mock_open): + """Simulate errors opening any file.""" + mock_open.side_effect = IOError() + + self.assertRaises(IOError, open, '/not/there_1', 'r') + self.assertRaises(IOError, open, '/not/there_2', 'r') + self.assertRaises(IOError, open, '/not/there_3', 'r') + + def test_error_on_all_but_one(self, mock_open): + """Setting a global error but allowing specific file/s.""" + mock_open.side_effect = IOError() + mock_open['/is/there'].side_effect = None + + self.assertRaises(IOError, open, '/not/there_1', 'r') + self.assertRaises(IOError, open, '/not/there_2', 'r') + + with open('/is/there', 'r'): + pass + + def test_error_list(self, mock_open): + """Setting a global side effect iterator.""" + mock_open.side_effect = [ValueError(), RuntimeError(), DEFAULT, ] + + self.assertRaises(ValueError, open, '/not/there_1', 'r') + self.assertRaises(RuntimeError, open, '/not/there_2', 'r') + + with open('/is/there', 'r'): + pass + + def test_error_on_read(self, mock_open): + """Simulate error when reading from file.""" + mock_open.return_value.read.side_effect = IOError() + + with open('/path/to/file', 'w') as handle: + with self.assertRaises(IOError): + handle.read() + + def test_error_on_write(self, mock_open): + """Simulate error when writing to file.""" + mock_open.return_value.write.side_effect = IOError() + + with open('/path/to/file', 'r') as handle: + with self.assertRaises(IOError): + handle.write('text') + + def test_error_by_name(self, mock_open): + """Raise an exception for a specific path.""" + mock_open['/path/to/error_file'].side_effect = IOError() + + # Trying to open a different file should be OK. + with open('/path/to/allowed_file', 'r'): + pass + + # But openning the bad file should raise an exception. + self.assertRaises(IOError, open, '/path/to/error_file', 'r') + + # Reset open's side effect and check read/write side effects. + mock_open['/path/to/error_file'].side_effect = None + mock_open['/path/to/error_file'].read.side_effect = IOError() + mock_open['/path/to/error_file'].write.side_effect = IOError() + with open('/path/to/error_file', 'r') as handle: + self.assertRaises(IOError, handle.read) + self.assertRaises(IOError, handle.write, 'Bad write') + + def test_read_return_value(self, mock_open): + """Set the return value from read().""" + mock_open.return_value.read_data = 'Some text' + mock_open.return_value.read.return_value = 'New text' + + with open('/path/to/file', 'w') as handle: + contents = handle.read() + + self.assertEqual('New text', contents) + + def test_read_side_effect(self, mock_open): + """Add a side effect to read(). + + Setting a side_effect can't change the return value. + """ + def set_sentinal(): + # pylint: disable=missing-docstring + sentinal[0] = True + + # This doesn't work. + return 'Text from side_effect' + + # If we define contents as a 'simple' variable (just None, for example) + # the assignment inside fake_write() will assign to a local variable + # instead of the 'outer' contents variable. + sentinal = [False, ] + mock_open.return_value.read_data = 'Some text' + mock_open.return_value.read.side_effect = set_sentinal + + with open('/path/to/file', 'w') as handle: + contents = handle.read() + + self.assertEqual('Some text', contents) + self.assertTrue(sentinal[0]) + + def test_write_side_effect(self, mock_open): + """Add a side effect to write().""" + def set_sentinal(data): + # pylint: disable=missing-docstring + sentinal[0] = True + + # Avoid uninitialized assignment (see test_read_side_effect()). + sentinal = [False, ] + mock_open.return_value.write.side_effect = set_sentinal + + with open('/path/to/file', 'w') as handle: + handle.write('Some text') + + self.assertEqual('Some text', handle.read_data) + self.assertTrue(sentinal[0]) + + def test_multiple_files(self, mock_open): + """Test different side effects for different files.""" + mock_open['fail_on_open'].side_effect = IOError() + mock_open['fail_on_read'].read.side_effect = IOError() + mock_open['fail_on_write'].write.side_effect = IOError() + + with open('success', 'w') as handle: + handle.write('some text') + + self.assertRaises(IOError, open, 'fail_on_open', 'rb') + + with open('fail_on_read', 'r') as handle: + self.assertRaises(IOError, handle.read) + + with open('fail_on_write', 'w') as handle: + self.assertRaises(IOError, handle.write, 'not to be written') + + with open('success', 'r') as handle: + self.assertEqual('some text', handle.read()) + + +class TestAPI(unittest.TestCase): + """Test conformance to mock library's API.""" + def test_read_data(self): + """Check passing of `read_data` to the constructor.""" + mock_open = MockOpen(read_data='Data from the file') + + with patch(OPEN, mock_open): + with open('/path/to/file', 'r') as handle: + contents = handle.read() + + self.assertEqual('Data from the file', contents) + + def test_reset_mock(self): + """Check that reset_mock() works.""" + # Reset globally for all file mocks. + mock_open = MockOpen(read_data='Global') + mock_open['/path/to/file'].read_data = 'File-specific' + mock_open.reset_mock() + + with patch(OPEN, mock_open): + with open('/path/to/file', 'r') as handle: + self.assertEqual('', handle.read()) + + # Reset a for a specific file mock. + mock_open = MockOpen(read_data='Global') + mock_open['/path/to/file'].read_data = 'File-specific' + mock_open['/path/to/file'].reset_mock() + + with patch(OPEN, mock_open): + with open('/path/to/file', 'r') as handle: + self.assertEqual('', handle.read()) + + with open('/path/to/other/file', 'r') as handle: + self.assertEqual('Global', handle.read()) + + +class TestModes(unittest.TestCase): + """Test different modes behavior.""" + @staticmethod + @patch(OPEN, new_callable=MockOpen) + def test_default_mode(mock_open): + """Default mode is 'r'.""" + with open('/path/to/file') as _: + pass + + mock_open.assert_called_once_with('/path/to/file', 'r') + + @patch(OPEN, new_callable=MockOpen) + def test_open_as_text(self, mock_open): + """Read/write to file as text (no 'b').""" + with open('/path/to/empty_file', 'r') as handle: + contents = handle.read() + + self.assertIsInstance(contents, str) + self.assertEqual('', contents) + + mock_open['/path/to/file'].read_data = 'Contents' + + with open('/path/to/file', 'r') as handle: + contents = handle.read() + + self.assertIsInstance(contents, str) + self.assertEqual('Contents', contents) + + with open('/path/to/file', 'w') as handle: + handle.write('New contents') + + self.assertEqual('New contents', handle.read_data) + + @patch(OPEN, new_callable=MockOpen) + def test_open_as_binary(self, mock_open): + """Read/write to file as binary data (with 'b').""" + with open('/path/to/empty_file', 'rb') as handle: + contents = handle.read() + + self.assertIsInstance(contents, bytes) + self.assertEqual(b'', contents) + + mock_open['/path/to/file'].read_data = b'Contents' + + with open('/path/to/file', 'rb') as handle: + contents = handle.read() + + self.assertIsInstance(contents, bytes) + self.assertEqual(b'Contents', contents) + + with open('/path/to/file', 'wb') as handle: + handle.write(b'New contents') + + self.assertEqual(b'New contents', handle.read_data) + + @patch(OPEN, new_callable=MockOpen) + def test_different_opens(self, _): + """Open the same file as text/binary.""" + with open('/path/to/file', 'w') as handle: + handle.write('Textual content') + + self.assertIsInstance(handle.read_data, str) + + with open('/path/to/file', 'rb') as handle: + contents = handle.read() + + self.assertIsInstance(contents, bytes) + self.assertEqual(b'Textual content', contents) + + with open('/path/to/file', 'r') as handle: + contents = handle.read() + + self.assertIsInstance(contents, str) + self.assertEqual('Textual content', contents) + + +class TestIssues(unittest.TestCase): + """Test cases related to issues on GitHub. + + See https://github.com/nivbend/mock-open + """ + def test_issue_1(self): + """Setting a side effect on a specific open() shouldn't affect + consecutive calls. + """ + mock_open = MockOpen() + mock_open['fail_on_open'].side_effect = IOError() + + with patch(OPEN, mock_open): + with self.assertRaises(IOError): + open('fail_on_open', 'rb') + + with open('success', 'r') as handle: + self.assertEqual('', handle.read()) + + def test_issue_3(self): + """Position in file should be set to 0 after the call to `open`.""" + mock_open = MockOpen(read_data='some content') + + with patch(OPEN, mock_open): + handle = open('/path/to/file', 'r') + self.assertEqual(0, handle.tell()) + self.assertEqual('some content', handle.read()) + + @staticmethod + @patch(OPEN, new_callable=MockOpen) + def test_issue_4(mock_open): + """Assert relative calls after consecutive opens.""" + with open('/path/to/file', 'r') as _: + pass + + mock_open.reset_mock() + + with open('/path/to/file', 'r') as _: + pass + + # The key here is that the last three calls objects are `call()` + # instead of `call`. This is fixed by setting _new_name. + mock_open.assert_has_calls([ + call('/path/to/file', 'r'), + call().__enter__(), + call().__exit__(None, None, None), + call().close(), + ]) diff -r de5f5648be2d Lib/unittest/test/testmock/testwith.py --- a/Lib/unittest/test/testmock/testwith.py Tue Mar 15 17:24:13 2016 +0100 +++ b/Lib/unittest/test/testmock/testwith.py Tue Mar 15 21:46:11 2016 +0200 @@ -136,7 +136,7 @@ self.assertIs(patched, mock) open('foo') - mock.assert_called_once_with('foo') + mock.assert_called_once_with('foo', 'r') def test_mock_open_context_manager(self): @@ -146,8 +146,9 @@ with open('foo') as f: f.read() - expected_calls = [call('foo'), call().__enter__(), call().read(), - call().__exit__(None, None, None)] + expected_calls = [call('foo', 'r'), call().__enter__(), call().read(), + call().__exit__(None, None, None), + call().close()] self.assertEqual(mock.mock_calls, expected_calls) self.assertIs(f, handle) @@ -160,10 +161,10 @@ f.read() expected_calls = [ - call('foo'), call().__enter__(), call().read(), - call().__exit__(None, None, None), - call('bar'), call().__enter__(), call().read(), - call().__exit__(None, None, None)] + call('foo', 'r'), call().__enter__(), call().read(), + call().__exit__(None, None, None), call().close(), + call('bar', 'r'), call().__enter__(), call().read(), + call().__exit__(None, None, None), call().close(),] self.assertEqual(mock.mock_calls, expected_calls) def test_explicit_mock(self): @@ -218,9 +219,8 @@ # emulated mock = mock_open(read_data='foo\nbar\nbaz') with patch('%s.open' % __name__, mock, create=True): - h = open('bar') + h = open('bar', 'r') result = h.readlines() - self.assertEqual(result, ['foo\n', 'bar\n', 'baz']) @@ -252,14 +252,6 @@ self.assertEqual(result, [b'abc\n', b'def\n', b'ghi\n']) - def test_mock_open_read_with_argument(self): - # At one point calling read with an argument was broken - # for mocks returned by mock_open - some_data = 'foo\nbar\nbaz' - mock = mock_open(read_data=some_data) - self.assertEqual(mock().read(10), some_data) - - def test_interleaved_reads(self): # Test that calling read, readline, and readlines pulls data # sequentially from the data we preload with @@ -282,7 +274,7 @@ def test_overriding_return_values(self): mock = mock_open(read_data='foo') - handle = mock() + handle = mock('/path/to/file') handle.read.return_value = 'bar' handle.readline.return_value = 'bar'