在单元测试中模拟 FTP
Mocking FTP in unit test
假设我想测试这个非常复杂的函数:
def func(hostname, username, password):
ftp = FTP(hostname, username, password)
ftp.retrbinary('RETR README', open('README', 'wb').write)
其中一项测试是:
@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
但是,这将创建一个名为 README 的本地文件,我显然不想要它。
有没有办法mock/patchopen
不创建文件?
显然,作为一种解决方法,我可以确保将文件写入 temporary directory,我可以将其作为参数传递给 func
或在 func
中创建,并且return.
注意使用装饰器@patch('__builtin__.open')
,会产生以下期望:
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'
我将回调传递给 ftp.retrbinary
而不是函数调用。
因此,考虑到您不关心打开时会发生什么,您可以直接模拟它以使其停止写入。为此,您可以采用与模拟 FTP
时类似的方法。因此,考虑到这一点,您可以像这样设置测试代码:
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()
正如你所看到的,我们在这里所做的是我们正在模拟我们正在测试的地方。因此,考虑到这一点,我们正在模拟 open
和 FTP
关于 my_code
。
现在在 my_code
内,什么都没有改变:
from ftplib import FTP
def func(hostname, username, password):
ftp = FTP(hostname, username, password)
ftp.retrbinary('RETR README', open('README', 'wb').write)
运行 这个测试套件成功返回。
另一种方法涉及使用 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()
假设我想测试这个非常复杂的函数:
def func(hostname, username, password):
ftp = FTP(hostname, username, password)
ftp.retrbinary('RETR README', open('README', 'wb').write)
其中一项测试是:
@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
但是,这将创建一个名为 README 的本地文件,我显然不想要它。
有没有办法mock/patchopen
不创建文件?
显然,作为一种解决方法,我可以确保将文件写入 temporary directory,我可以将其作为参数传递给 func
或在 func
中创建,并且return.
注意使用装饰器@patch('__builtin__.open')
,会产生以下期望:
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'
我将回调传递给 ftp.retrbinary
而不是函数调用。
因此,考虑到您不关心打开时会发生什么,您可以直接模拟它以使其停止写入。为此,您可以采用与模拟 FTP
时类似的方法。因此,考虑到这一点,您可以像这样设置测试代码:
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()
正如你所看到的,我们在这里所做的是我们正在模拟我们正在测试的地方。因此,考虑到这一点,我们正在模拟 open
和 FTP
关于 my_code
。
现在在 my_code
内,什么都没有改变:
from ftplib import FTP
def func(hostname, username, password):
ftp = FTP(hostname, username, password)
ftp.retrbinary('RETR README', open('README', 'wb').write)
运行 这个测试套件成功返回。
另一种方法涉及使用 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()