简体   繁体   中英

How can i test for exception using pytest

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 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:

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM