Home > Mobile >  Test the Except Block, Mock the return of a function be an exception python pytest
Test the Except Block, Mock the return of a function be an exception python pytest

Time:04-02

i'm writing a test of this function

 def create_folder_if_not_exists(
         sdk: looker_sdk,
         folder_name: str,
         parent_folder_name: str) -> dict:
      folder = sdk.search_folders(name=folder_name)[0]
      try:
         parent_id = sdk.search_folders(name=parent_folder_name)[0].id
 
         logger.info(f'Creating folder "{folder_name}"')
         folder = sdk.create_folder(
                  body=models.CreateFolder(
                      name=folder_name,
                      parent_id=parent_id
                  )
              )
          return folder
 
      except looker_sdk.error.SDKError as err:
          logger.error(err.args[0])
          return folder

This is my current test, using the python pytest library, but i keep getting this for my test Failed: DID NOT RAISE <class 'looker_sdk.error.SDKError'>

def test_create_folder_if_not_exists_parent1(mocker):
      # Tests if a folder has parent id of 1 we raise an exception
      sdk = fake_methods_data.MockSDK()
      sf_data = fake_methods_data.MockSearchFolder(
          name='goog', parent_id=1, id=3)
      mocker.patch.object(sdk, "search_folders")
      mocker.patch.object(sdk, "create_folder",
                          side_effect=[looker_sdk.error.SDKError])
      sdk.search_folders.return_value = [sf_data]
      
      with pytest.raises(looker_sdk.error.SDKError) as err:
          test = fc.create_folder_if_not_exists(
          sdk=sdk, folder_name='googn', parent_folder_name='1')

      assert str(err.value) == 'test'
 
      assert test.parent_id == 1
      assert test.name == 'googn'

Does anyone know how to force a function to return a class error using pytest ? I've been looking at this [stackoverflow] (Mocking a function to raise an Exception to test an except block) but am struggling to get it to work. Hoping for some other thoughts.

CodePudding user response:

This sounds like something I have done for work (open-source software dev stuff). In my case, I needed to test an except block raised when an executable file could not be run on a particular OS version. In our testing framework we use pytest and monkeypatch to test things. I've included the relevant bits of code below, along with some explanation about what is happening. I think this is probably what you mean by 'patch the sdk error', and I believe that is probably what you need to do. If anything is unclear, or you have more questions, let me know.

In conftest.py I define pytest fixtures that get used for tests in more than one test file. Here, I mock the scenario I want to test, using monkeypatch to fake the results I want from the parts of the get_version() function I'm not trying to test.

# conftest.py

import subprocess
import shutil
import os
import re
import platform

from pathlib import Path

import pytest


@pytest.fixture
def executable_incompatible_with_os(monkeypatch):
    """
    Mocks an executable file that is incompatible with the OS.

    (This situation likely only applies to blastall.)
    """

    def mock_which(*args, **kwargs):
        """Mock an absolute file path."""
        return args[0]

    def mock_isfile(*args, **kwargs):
        """Mock a call to `os.path.isfile()`."""
        return True

    def mock_access(*args, **kwargs):
        """Mock a call to `os.access()`."""
        return True

    def mock_subprocess(*args, **kwargs):
        """Mock a call to `subprocess.run()` with an incompatible program."""
        raise OSError

    # Replace calls to existing methods with my mocked versions
    monkeypatch.setattr(shutil, "which", mock_which)
    monkeypatch.setattr(Path, "is_file", mock_isfile)
    monkeypatch.setattr(os.path, "isfile", mock_isfile)
    monkeypatch.setattr(os, "access", mock_access)
    monkeypatch.setattr(subprocess, "run", mock_subprocess)

In test_aniblastall.py I test parts of aniblastall.py. In this case, I'm testing the behaviour when an OSError is raised; the code that raises the error in the test is in conftest.py. The entire pytest fixture I defined there is passed as a parameter to the test.

# test_aniblastall.py

from pathlib import Path
import unittest

# Test case 4: there is an executable file, but it will not run on the OS
def test_get_version_os_incompatible(executable_incompatible_with_os):
    """Test behaviour when the program can't run on the operating system.
    This will happen with newer versions of MacOS."""
    test_file_4 = Path("/os/incompatible/blastall")
    assert (
        aniblastall.get_version(test_file_4)
        == f"blastall exists at {test_file_4} but could not be executed"
    )

aniblastall.py contains the function the error should be raised from.

# aniblastall.py

import logging
import os
import platform
import re
import shutil
import subprocess

from pathlib import Path

def get_version(blast_exe: Path = pyani_config.BLASTALL_DEFAULT) -> str:
    """
    The following circumstances are explicitly reported as strings

    - no executable at passed path
    - non-executable file at passed path (this includes cases where the user doesn't have execute permissions on the file)
    - no version info returned
    - executable cannot be run on this OS
    """
    logger = logging.getLogger(__name__)

    try:
        blastall_path = Path(shutil.which(blast_exe))  # type:ignore
    except TypeError:
        return f"{blast_exe} is not found in $PATH"

    if not blastall_path.is_file():  # no executable
        return f"No blastall at {blastall_path}"

    # This should catch cases when the file can't be executed by the user
    if not os.access(blastall_path, os.X_OK):  # file exists but not executable
        return f"blastall exists at {blastall_path} but not executable"

    if platform.system() == "Darwin":
        cmdline = [blast_exe, "-version"]
    else:
        cmdline = [blast_exe]

    try:
        result = subprocess.run(
            cmdline,  # type: ignore
            shell=False,
            stdout=subprocess.PIPE,  # type: ignore
            stderr=subprocess.PIPE,
            check=False,  # blastall doesn't return 0
        )

    except OSError:
        logger.warning("blastall executable will not run", exc_info=True)
        return f"blastall exists at {blastall_path} but could not be executed"

    version = re.search(  # type: ignore
        r"(?<=blastall\s)[0-9\.]*", str(result.stderr, "utf-8")
    ).group()

    if 0 == len(version.strip()):
        return f"blastall exists at {blastall_path} but could not retrieve version"

    return f"{platform.system()}_{version} ({blastall_path})"

CodePudding user response:

This is super valuable @baileythegreen, however my problem was far simpler. I had an if/else and the else had the try/catch error code piece. I was so focused on that I didn't check the simple part of if it was even getting to the else. :(

  • Related