Reputation: 18246
Let's say that I want to test this oh-so-complex function:
def func(hostname, username, password):
ftp = FTP(hostname, username, password)
ftp.retrbinary('RETR README', open('README', 'wb').write)
One of the tests would be:
@patch('FTP')
def test_func_happy_path():
mock_ftp = Mock()
mock_ftp.retrbinary = Mock()
MockFTP.return_value = mock_ftp()
func('localhost', 'fred', 's3Kr3t')
assert mock_ftp.retrbinary.called
However, this will create a local file called README which I clearly do not want.
Is there a way to mock/patch open
so that no files are created?
Clearly as a work around, I can make sure that the file is written to a temporary directory which I can either pass as an argument to func
or create within func
and return.
Note that using the decorator @patch('__builtin__.open')
, the following expectation is raised:
self = <Mock name=u'open()' spec='FTP' id='51439824'>, name = 'write'
def __getattr__(self, name):
if name in ('_mock_methods', '_mock_unsafe'):
raise AttributeError(name)
elif self._mock_methods is not None:
if name not in self._mock_methods or name in _all_magics:
> raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'write'
I am passing a callback to ftp.retrbinary
and not a function call.
Upvotes: 2
Views: 3738
Reputation: 26570
So, considering that you do not care about what happens with your open, you can straight out mock it so it stops writing. To do this you can follow the similar approach you did with mocking your FTP
. So, with that in mind, you can set up your test code like this:
import unittest
from mock import patch, Mock
from my_code import func
class SirTestsAlot(unittest.TestCase):
@patch('my_code.open')
@patch('my_code.FTP')
def test_func_happy_path(self, MockFTP, m_open):
MockFTP.return_value = Mock()
mock_ftp_obj = MockFTP()
m_open.return_value = Mock()
func('localhost', 'fred', 's3Kr3t')
assert mock_ftp_obj.retrbinary.called
assert m_open.called
# To leverage off of the other solution proposed, you can also
# check assert called with here too
m_open.assert_called_once_with('README', 'wb')
if __name__ == '__main__':
unittest.main()
As you can see, what we are doing here is that we are mocking with respect to where we are testing. So, with that in mind, we are mocking out open
and FTP
with respect to my_code
.
Now within my_code
, nothing was changed:
from ftplib import FTP
def func(hostname, username, password):
ftp = FTP(hostname, username, password)
ftp.retrbinary('RETR README', open('README', 'wb').write)
Running this test suite comes back successfully.
Upvotes: 3
Reputation: 5340
Another approach involves using mock_open:
from unittest.mock import patch, mock_open
import ftplib
def func(hostname, username, password):
ftp = ftplib.FTP(hostname, username, password)
ftp.retrbinary('RETR README', open('README', 'wb').write)
@patch('ftplib.FTP')
def test_func_happy_path(MockFTP):
mock_ftp = MockFTP.return_value # returns another `MagicMock`
with patch('__main__.open', mock_open(), create=True) as m:
func('localhost', 'fred', 's3Kr3t')
assert mock_ftp.retrbinary.called
m.assert_called_once_with('README', 'wb')
test_func_happy_path()
Upvotes: 2