how to test that an Exception is raised



When an error happens in Python, an Exception is raised to break execution of the program, this means nothing past the line that caused it will run.

It is useful because there is a problem to be solved to continue as expected, it can be a pain when it causes the program to stop early. What if I want it to run even with errors? I might want to give messages to the user who does not care about or understand the details of the error.

Exception Handling is a way to deal with this, it allows programs to make decisions when one happens.

test_catching_module_not_found_error_in_tests

red: make it fail

  • I open a terminal to run makePythonTdd.sh with exceptions as the name of the project

    ./makePythonTdd.sh exceptions
    

    on Windows without Windows Subsystem Linux use makePythonTdd.ps1

    ./makePythonTdd.ps1 exceptions
    

    it makes the folders and files that are needed, installs packages, runs the first test, and the terminal shows AssertionError

    E       AssertionError: True is not false
    
    tests/test_exceptions.py:7: AssertionError
    
  • I hold ctrl (windows/linux) or option (mac) on the keyboard and use the mouse to click on tests/test_exceptions.py:7 to open it in the editor

  • then change True to False to make the test pass

  • and change test_failure to test_catching_module_not_found_error_in_tests with an import statement

    class TestExceptions(unittest.TestCase):
    
        def test_catching_module_not_found_error_in_tests(self):
            import does_not_exist
    

    the terminal shows ModuleNotFoundError

    ModuleNotFoundError: No module named 'does_not_exist'
    

green: make it pass

  • I add it to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # ModuleNotFoundError
    
  • I can make does_not_exist.py to solve the problem but I want to catch or handle it in the test. I add the unittest.TestCase.assertRaises method which checks that the code in its context raises the Exception it is given

    def test_catching_module_not_found_error_in_tests(self):
        with self.assertRaises(ModuleNotFoundError):
            import does_not_exist
    

    and the test passes


test_catching_name_error_in_tests

red: make it fail

  • I add a failing test

    def test_catching_name_error_in_tests(self):
        does_not_exist
    

    and the terminal shows NameError

    NameError: name 'does_not_exist' is not defined
    
  • I add it to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # ModuleNotFoundError
    # NameError
    

green: make it pass

I add unittest.TestCase.assertRaises

def test_catching_name_error_in_tests(self):
    with self.assertRaises(NameError):
        does_not_exist

and the terminal shows passing tests


test_catching_attribute_error_in_tests

red: make it fail

  • I add another failing test

    def test_catching_attribute_error_in_tests(self):
        src.exceptions.does_not_exist
    

    the terminal shows NameError

    NameError: name 'src' is not defined
    
  • I add an import statement for the module

    import src.exceptions
    import unittest
    

    which gives me AttributeError

    AttributeError: module 'src.exceptions' has no attribute 'does_not_exist'
    

    because I tried to get something that does not exist from something that exists

  • I add the error to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # ModuleNotFoundError
    # NameError
    # AttributeError
    

green: make it pass

then I add a call to the unittest.TestCase.assertRaises method

def test_catching_attribute_error_in_tests(self):
    with self.assertRaises(AttributeError):
        src.exceptions.does_not_exist

and the terminal shows passing tests


test_catching_type_error_in_tests

red: make it fail

  • I add a failing test

    def test_catching_type_error_in_tests(self):
        src.exceptions.function_name('argument')
    

    and get AttributeError

    AttributeError: module 'src.exceptions' has no attribute 'function_name'
    
  • I add the name to exceptions.py

    function_name
    

    and the terminal shows NameError

    NameError: name 'function_name' is not defined
    

    then I assign it to None to define it

    function_name = None
    

    and get TypeError

    TypeError: 'NoneType' object is not callable
    
  • I add it to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # ModuleNotFoundError
    # NameError
    # AttributeError
    # TypeError
    

green: make it pass

then I add unittest.TestCase.assertRaises to the test

def test_catching_type_error_in_tests(self):
    with self.assertRaises(TypeError):
        src.exceptions.function_name('argument')

and the terminal shows passing tests

refactor: make it better

If I make function_name a function

def function_name():
    return None

the terminal still shows passing tests, I have the TypeError because the call sends an argument and the function does not accept input. When I add a parameter to the definition

def function_name(argument):
    return None

the terminal shows AssertionError

AssertionError: TypeError not raised

because the function call matches the signature. I undo the change to get back the TypeError and make the test pass

def function_name():
    return None

test_catching_index_error_in_tests

red: make it fail

  • I make a list in a new test

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
    

    the first item in it has 0 as its index

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        a_list[0]
    

    the terminal shows green. The index for the last item is the total number of items minus 1, which is 3 in this case

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        a_list[3]
    

    still green. When I use a number that is bigger than the index for the last item

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        a_list[4]
    

    the terminal shows IndexError

    IndexError: list index out of range
    
  • I add it to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # ModuleNotFoundError
    # NameError
    # AttributeError
    # TypeError
    # IndexError
    

green: make it pass

  • then I add assertRaises

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        with self.assertRaises(IndexError):
            a_list[4]
    

    and the test passes

  • I can also index with negative numbers, the one for the last item in the list is -1, think reading from right to left

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        with self.assertRaises(IndexError):
            a_list[4]
        a_list[-1]
    

    the terminal still shows passing tests. The index for the first item is negative the total number of items, -4 in this case

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        with self.assertRaises(IndexError):
            a_list[4]
        a_list[-4]
    

    still green. When I use a negative number that is outside the range

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        with self.assertRaises(IndexError):
            a_list[4]
        a_list[-5]
    

    the terminal shows IndexError

    IndexError: list index out of range
    

    I add the assertRaises

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        with self.assertRaises(IndexError):
            a_list[4]
        with self.assertRaises(IndexError):
            a_list[-5]
    

    and the terminal shows green again

  • It looks like there is a duplication of the assertRaises but it is not, even though the test is green when I remove the second one

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        with self.assertRaises(IndexError):
            a_list[4]
            a_list[-5]
    

    at the end of the chapter I show why this is not a repetition, I undo the change for now

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        with self.assertRaises(IndexError):
            a_list[4]
        with self.assertRaises(IndexError):
            a_list[-5]
    

test_catching_key_error_in_tests

red: make it fail

  • I add a dictionary to a new test

    def test_catching_key_error_in_tests(self):
        {'key': 'value'}
    

    when I try to get the value for a key that exists

    def test_catching_key_error_in_tests(self):
        {'key': 'value'}['key']
    

    the terminal shows green and when I use a key that does not exist

    def test_catching_key_error_in_tests(self):
        {'key': 'value'}['does_not_exist']
    

    I get KeyError

    KeyError: 'does_not_exist'
    
  • another one for the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # ModuleNotFoundError
    # NameError
    # AttributeError
    # TypeError
    # IndexError
    # KeyError
    

green: make it pass

I add assertRaises to the test

def test_catching_key_error_in_tests(self):
    with self.assertRaises(KeyError):
        {'key': 'value'}['does_not_exist']

and the terminal shows green again

test_catching_zero_division_error_in_tests

red: make it fail

  • I add another failing test for something that happened in how to make a calculator

    def test_catching_zero_division_error_in_tests(self):
        1 / 0
    

    any number divided by 0 and the terminal shows a ZeroDivisionError

    ZeroDivisionError: division by zero
    
  • I add it to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # ModuleNotFoundError
    # NameError
    # AttributeError
    # TypeError
    # IndexError
    # KeyError
    # ZeroDivisionError
    

green: make it pass

then add the assertRaises method to the test

def test_catching_zero_division_error_in_tests(self):
    with self.assertRaises(ZeroDivisionError):
        1 / 0

and the terminal shows passing tests


test_catching_exceptions_in_tests

red: make it fail

  • I add a failing test

    def test_catching_exceptions_in_tests(self):
        raise Exception
    

    the terminal shows Exception which is the mother of the Exceptions encountered so far, they inherit from it

    Exception
    
  • I can use the raise statement to cause any Exception

    def test_catching_exceptions_in_tests(self):
        raise AssertionError
    

    and the terminal will show it

    AssertionError
    

green: make it pass

when I add the assertRaises method to the test

def test_catching_exceptions_in_tests(self):
    with self.assertRaises(Exception):
        raise Exception

the terminal shows all tests are passing.

To review, the assertRaises method checks that the code in its context raises the Exception it is given.

refactor: make it better

  • I can use Exception to catch any of the Exceptions that inherit from it, its children if you will

    def test_catching_key_error_in_tests(self):
        with self.assertRaises(Exception):
            {'key': 'value'}['does_not_exist']
    
    
    def test_catching_zero_division_error_in_tests(self):
        with self.assertRaises(Exception):
            1 / 0
    

    all the tests are still green. The problem with using Exception to catch its children, is it does not tell anyone that reads the code what the actual error is or which line caused it, especially when there is more than one line of code in the context. It is better to be specific, from the Zen of python: Explicit is better than implicit

  • I cannot use siblings or cousins to catch other Exceptions

    def test_catching_key_error_in_tests(self):
        with self.assertRaises(ModuleNotFoundError):
            {'key': 'value'}['does_not_exist']
    

    shows KeyError

    KeyError: 'does_not_exist'
    

    because it is not ModuleNotFoundError

  • As promised here is why the second AssertRaises in test_catching_index_error_in_tests is not a repetition, even though the test still passes when I remove it

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        with self.assertRaises(IndexError):
            a_list[4]
            a_list[-5]
    

    If I add a raise statement between the 2 lines

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        with self.assertRaises(IndexError):
            a_list[4]
            raise Exception
            a_list[-5]
    

    the terminal still shows a passing test, even though Exception is not IndexError, it looks like the assertRaises exits after the first line that causes IndexError. When I move the raise statement above it

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        with self.assertRaises(IndexError):
            raise Exception
            a_list[4]
            a_list[-5]
    

    the terminal shows Exception

    Exception
    

    because it is not IndexError

  • as a rule of thumb I write one line of code for one how to test that an Exception is raised to know which line caused which error

    def test_catching_index_error_in_tests(self):
        a_list = [1, 2, 3, 'n']
        with self.assertRaises(IndexError):
            a_list[4]
        with self.assertRaises(IndexError):
            a_list[-5]
    

review

I have a way to catch Exceptions in tests and ran into the following

Would you like to test handling Exceptions in programs?


how to handle Exceptions: tests and solutions