Since the block that we need to mock is a context manager (here being ftplib.FTP
), we have to gain control over the following to direct the flow to either the success scenario or the exception scenario.
contextmanager.__enter__()
... The value returned by this method is bound to the identifier in
the as
clause of with
statements using this context manager ...
contextmanager.__exit__(exc_type, exc_val, exc_tb)
... Returning a true
value from this method will cause the with
statement to suppress the exception and continue execution with the
statement immediately following the with
statement. Otherwise the
exception continues propagating after this method has finished
executing. ...
Here, we would mock ftplib.FTP
and ftplib.FTP.retrbinary
to control whether it would pass through the success scenario or the exception scenario.
test_ftp.py
import ftplib
from unittest import mock
import pytest
# Simplified version to focus on the test logic of mocking FTP
def _fetch_files(ftp_server: str, ftp_dir: str, file_name: str) -> None:
with ftplib.FTP(ftp_server, timeout=3.0) as ftp:
print(f"FTP object {type(ftp)} {ftp}")
ftp.login()
ftp.cwd(ftp_dir)
try:
with open(file_name, 'wb') as fp:
ftp.retrbinary(f'RETR {file_name}', fp.write)
print(f'writing file: {file_name}')
except ftplib.error_perm:
print(f'{file_name} not found')
# Raise an error so that we can see if the failure actually happened
raise FileNotFoundError(f'{file_name} not found')
ftp.quit()
@mock.patch("ftplib.FTP") # Mock FTP so that we wouldn't access the real world files
def test_fetch_files_success(mock_ftp):
# With all FTP operations mocked to run ok, the flow should go through the success scenario.
_fetch_files('ftp.ncdc.noaa.gov', 'pub/data/noaa/2016', '123.gz')
@mock.patch("ftplib.FTP") # Mock FTP so that we wouldn't access the real world files
def test_fetch_files_exception(mock_ftp):
"""
Mock FTP:retrbinary to raise an exception. This should go through the exception scenario.
1. mock_ftp.return_value
The FTP object, here being the <ftplib.FTP(ftp_server, timeout=3.0)>
2. .__enter__.return_value
The object to be bound in the <as> clause, here being the <ftp> variable in <with ftplib.FTP(ftp_server, timeout=3.0) as ftp:>
3. .retrbinary.side_effect
The behavior if the bound object <ftp> is used to call <ftp.retrbinary(...)>, which here was configured to raise the exception <ftplib.error_perm>
"""
mock_ftp.return_value.__enter__.return_value.retrbinary.side_effect = ftplib.error_perm
with pytest.raises(FileNotFoundError) as error:
_fetch_files('ftp.ncdc.noaa.gov', 'pub/data/noaa/2016', '123.gz')
assert str(error.value) == '123.gz not found'
Logs
$ pytest -rP
================================================================================================= PASSES ==================================================================================================
________________________________________________________________________________________ test_fetch_files_success _________________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
FTP object <class 'unittest.mock.MagicMock'> <MagicMock name='FTP().__enter__()' id='140376736032224'>
writing file: 123.gz
_______________________________________________________________________________________ test_fetch_files_exception ________________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
FTP object <class 'unittest.mock.MagicMock'> <MagicMock name='FTP().__enter__()' id='140376735714608'>
123.gz not found
============================================================================================ 2 passed in 0.03s ============================================================================================
References: