How can i test the exception branch of my _fetch_files without calling an actual ftp server? Below is my implementation code and the current test that is testing the exception branch.
def _fetch_files(ftp_server:str, ftp_dir:str, file_name:str, dir_path:pathlib.Path) -> None:
''' logs into noaa's ftp server and downloads to memory `.gz` files for a given year, for a given file_name
Args:
ftp_server: string of ftp server
ftp_dir: dir_path containing `.gz` files
file_name: weather station by `.gz` file
dir_path: the dir path to which the files will be saved to
Returns:
None
'''
with ftplib.FTP(ftp_server, timeout=3.0) as ftp:
ftp.login()
ftp.cwd(ftp_dir)
make_raw_dir(dir_path)
try:
with open(file_name, 'wb') as fp:
ftp.retrbinary(f'RETR {file_name}', fp.write)
logger.info(f'writing file: {file_name}')
except ftplib.error_perm:
logger.error(f'{file_name} not found')
ftp.quit()
def test_fetch_files_exception(tmp_path):
tmp_dir_path = tmp_path / 'sub'
tmp_dir_path.mkdir()
with mock.patch('module_three.utils_IO_bound._fetch_files', side_effect=Exception) as mock_req:
with pytest.raises(Exception):
assert _fetch_files('ftp.ncdc.noaa.gov', 'pub/data/noaa/2016', '123.gz', tmp_dir_path) == '123.gz not found'
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 ofwith
statements using this context manager...
contextmanager.__exit__(exc_type, exc_val, exc_tb)
... Returning a
true
value from this method will cause thewith
statement to suppress the exception and continue execution with the statement immediately following thewith
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:
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.